import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; /** * Next.js 16 代理层 * * 1. API 请求速率限制 * 2. 页面身份验证 */ /* ==================== 速率限制 ==================== */ interface RateLimitEntry { count: number; windowStart: number; } const rateLimitStore = new Map(); /** 不需要速率限制的路径 */ const EXCLUDED_PREFIXES = ['/api/v1/config', '/epay/', '/lpay/']; /** 速率限制规则: [最大请求数, 窗口时长(ms)] */ const RATE_LIMITS: Record = { '/api/v1/oauth/login': [1, 5000], '/api/v1/oauth/callback': [1, 5000], }; /** 默认限制: 60次/60秒 */ const DEFAULT_RATE_LIMIT: [number, number] = [60, 60000]; function getRateLimit(pathname: string): [number, number] { if (RATE_LIMITS[pathname]) return RATE_LIMITS[pathname]; const match = Object.keys(RATE_LIMITS) .filter((prefix) => pathname.startsWith(prefix)) .sort((a, b) => b.length - a.length)[0]; return match ? RATE_LIMITS[match] : DEFAULT_RATE_LIMIT; } function shouldRateLimit(pathname: string): boolean { return !EXCLUDED_PREFIXES.some((prefix) => pathname.startsWith(prefix)); } function checkRateLimit( identifier: string, pathname: string, ): [boolean, number] { const [maxRequests, windowMs] = getRateLimit(pathname); const key = `${identifier}:${pathname}`; const now = Date.now(); const entry = rateLimitStore.get(key); if (!entry || now - entry.windowStart >= windowMs) { rateLimitStore.set(key, { count: 1, windowStart: now }); return [true, 0]; } entry.count++; if (entry.count > maxRequests) { return [false, Math.ceil((windowMs - (now - entry.windowStart)) / 1000)]; } return [true, 0]; } if (typeof setInterval !== 'undefined') { setInterval(() => { const now = Date.now(); for (const [key, entry] of rateLimitStore.entries()) { if (now - entry.windowStart > 120000) rateLimitStore.delete(key); } }, 60000); } /* ==================== 代理主函数 ==================== */ export function proxy(request: NextRequest) { const { pathname, search } = request.nextUrl; const sessionCookieName = process.env.WAVELET_SESSION_COOKIE_NAME || 'openflare_session_id'; const sessionCookie = request.cookies.get(sessionCookieName); /* API 请求:速率限制后放行 */ if (pathname.startsWith('/api/')) { const rateLimitEnabled = process.env.WAVELET_RATE_LIMIT_ENABLED === 'true'; if (rateLimitEnabled && shouldRateLimit(pathname)) { const identifier = sessionCookie?.value || request.headers.get('x-forwarded-for')?.split(',')[0].trim() || request.headers.get('x-real-ip') || 'anonymous'; const [allowed, waitTime] = checkRateLimit(identifier, pathname); if (!allowed) { return NextResponse.json( { error_code: 'RATE_LIMITED', error_msg: `请求过于频繁,请 ${waitTime} 秒后重试`, }, { status: 429, headers: { 'Retry-After': String(waitTime) } }, ); } } return NextResponse.next(); } /* 页面请求:默认私域,仅登录流程入口无需 session */ const authEntryRoutes = ['/login', '/register', '/callback']; if (!authEntryRoutes.includes(pathname) && !sessionCookie) { const loginUrl = new URL('/login', request.url); loginUrl.searchParams.set('callbackUrl', pathname + search); return NextResponse.redirect(loginUrl); } return NextResponse.next(); } export const config = { matcher: [ '/api/:path*', '/((?!_next|favicon.ico|robots.txt|sitemap.xml|.*\\.(?:jpg|jpeg|gif|png|svg|ico|webp)).*)', ], };