import { type NextRequest, NextResponse } from "next/server"; import { getToken } from "next-auth/jwt"; import { isBlockedIP, getClientIP } from "@/lib/security/ip-blocklist"; import { checkRateLimit, checkRateLimitByIdentifier, rateLimiters, } from "@/lib/security/rate-limiter"; import { isSuspiciousRequest, logSuspiciousRequest } from "@/lib/security/request-monitor"; function isHighFrequencyReadApi(pathname: string, method: string) { if (method !== "GET") return false; if (/^\/api\/canvas\/[^/]+\/detail-archives$/.test(pathname)) return true; return [ "/api/models", "/api/canvas", "/api/nav-links", "/api/site-settings", "/api/messages", "/api/history", "/api/video-tasks", "/api/tutorial-videos", "/api/user/membership-summary", "/api/user/account-center", ].includes(pathname); } export async function proxy(request: NextRequest) { const { pathname } = request.nextUrl; const isSecure = process.env.AUTH_URL?.startsWith("https://") || request.nextUrl.protocol === "https:"; let cachedToken: Awaited> | null | undefined; const resolveToken = async () => { if (cachedToken !== undefined) { return cachedToken; } cachedToken = await getToken({ req: request, secret: process.env.AUTH_SECRET, secureCookie: isSecure, }); return cachedToken; }; // 🛡️ 安全检查1:阻止黑名单IP const clientIP = getClientIP(request); if (isBlockedIP(clientIP)) { console.warn(`[Security] Blocked request from blacklisted IP: ${clientIP}`, { pathname, method: request.method, userAgent: request.headers.get("user-agent"), }); return new NextResponse("Forbidden", { status: 403 }); } // 🛡️ 安全检查2:检测可疑请求模式 const suspiciousCheck = isSuspiciousRequest(request); if (suspiciousCheck.suspicious) { logSuspiciousRequest(request, suspiciousCheck.reason || "Unknown suspicious pattern"); // 对于可疑请求,记录但不立即阻止(避免误报) // 可以在这里添加更严格的检查 } // 🛡️ 安全检查3:速率限制 // 对API路径应用速率限制 if (pathname === "/api/chat") { const chatRateLimit = checkRateLimit(request, rateLimiters.chat); if (!chatRateLimit.allowed) { console.warn(`[Security] Chat rate limit exceeded for IP: ${clientIP}`, { pathname, resetTime: new Date(chatRateLimit.resetTime).toISOString(), }); return NextResponse.json( { error: "Too many chat requests", retryAfter: Math.ceil((chatRateLimit.resetTime - Date.now()) / 1000) }, { status: 429 } ); } } else if (pathname === "/api/files/upload" && request.method === "POST") { const token = await resolveToken(); const userId = typeof token?.sub === "string" && token.sub.length > 0 ? token.sub : "anonymous"; const uploadRateLimit = checkRateLimitByIdentifier( `${userId}:${request.method}:${pathname}`, rateLimiters.upload ); if (!uploadRateLimit.allowed) { const retryAfter = Math.max(1, Math.ceil((uploadRateLimit.resetTime - Date.now()) / 1000)); console.warn(`[Security] Upload rate limit exceeded for IP: ${clientIP}`, { pathname, method: request.method, userId, resetTime: new Date(uploadRateLimit.resetTime).toISOString(), }); return NextResponse.json( { error: "Too many upload requests", retryAfter }, { status: 429, headers: { "Retry-After": String(retryAfter) } } ); } } else if (pathname.startsWith("/api/canvas/") && request.method === "PUT") { const canvasRateLimit = checkRateLimit(request, rateLimiters.canvas); if (!canvasRateLimit.allowed) { console.warn(`[Security] Canvas rate limit exceeded for IP: ${clientIP}`, { pathname, resetTime: new Date(canvasRateLimit.resetTime).toISOString(), }); return NextResponse.json( { error: "Too many canvas requests", retryAfter: Math.ceil((canvasRateLimit.resetTime - Date.now()) / 1000) }, { status: 429 } ); } } else if ( pathname.startsWith("/api/realtime/") || pathname.startsWith("/api/fal/proxy") ) { const realtimeRateLimit = checkRateLimit(request, rateLimiters.realtime); if (!realtimeRateLimit.allowed) { console.warn(`[Security] Realtime rate limit exceeded for IP: ${clientIP}`, { pathname, resetTime: new Date(realtimeRateLimit.resetTime).toISOString(), }); return NextResponse.json( { error: "Too many realtime requests", retryAfter: Math.ceil((realtimeRateLimit.resetTime - Date.now()) / 1000) }, { status: 429 } ); } } else if (isHighFrequencyReadApi(pathname, request.method)) { const pollingRateLimit = checkRateLimit(request, rateLimiters.uiPolling); if (!pollingRateLimit.allowed) { console.warn(`[Security] UI polling rate limit exceeded for IP: ${clientIP}`, { pathname, resetTime: new Date(pollingRateLimit.resetTime).toISOString(), }); return NextResponse.json( { error: "Too many polling requests", retryAfter: Math.ceil((pollingRateLimit.resetTime - Date.now()) / 1000), }, { status: 429 } ); } } else if (pathname.startsWith("/api/")) { const rateLimit = checkRateLimit(request, rateLimiters.api); if (!rateLimit.allowed) { console.warn(`[Security] Rate limit exceeded for IP: ${clientIP}`, { pathname, resetTime: new Date(rateLimit.resetTime).toISOString(), }); return NextResponse.json( { error: "Too many requests", retryAfter: Math.ceil((rateLimit.resetTime - Date.now()) / 1000) }, { status: 429 } ); } } // 🛡️ 安全检查4:认证相关API的严格速率限制 if (pathname.startsWith("/api/auth/")) { const rateLimit = checkRateLimit(request, rateLimiters.auth); if (!rateLimit.allowed) { console.warn(`[Security] Auth rate limit exceeded for IP: ${clientIP}`); return NextResponse.json( { error: "Too many authentication attempts", retryAfter: Math.ceil((rateLimit.resetTime - Date.now()) / 1000) }, { status: 429 } ); } } // 允许访问 ping 端点 if (pathname.startsWith("/ping")) { return NextResponse.next(); } // 允许访问认证相关 API if (pathname.startsWith("/api/auth")) { return NextResponse.next(); } // 允许访问公开的网站设置 API(登录页面需要) if (pathname === "/api/site-settings") { return NextResponse.next(); } // 允许定时任务接口由服务端调用(具体鉴权由接口内部 CRON_SECRET 负责) if (pathname.startsWith("/api/jobs/")) { return NextResponse.next(); } // 允许公开访问动态 favicon(浏览器标签图标) if (pathname === "/api/favicon") { return NextResponse.next(); } // 允许访问公开的认证配置(登录弹窗需要) if (pathname === "/api/auth-settings") { return NextResponse.next(); } // 允许匿名访问新版首页灵感数据 if (pathname === "/api/featured" || pathname.startsWith("/api/featured/")) { return NextResponse.next(); } // 允许访问钉钉回调页面(public 文件夹中的文件) if (pathname === "/dingtalk-callback.html") { return NextResponse.next(); } // 允许 DuluPay 支付回调(服务端回调无 session cookie) if (pathname.startsWith("/api/payment/")) { return NextResponse.next(); } const token = await resolveToken(); const externalBase = process.env.AUTH_URL ?? `${request.nextUrl.protocol}//${request.headers.get("host") ?? request.nextUrl.host}`; // 未登录用户允许访问 /(新版首页)、/login、/register,其余需要登录 if (!token && !["/", "/login", "/register"].includes(pathname)) { // 对于 API 路由的非 GET 请求,直接返回 401 JSON 响应(不重定向) if (pathname.startsWith("/api/") && request.method !== "GET") { return NextResponse.json( { error: "未授权,请先登录" }, { status: 401 } ); } // 对于页面路由(GET 请求),重定向到登录页面 const loginUrl = new URL("/login", externalBase); // 保存当前路径作为回调 URL loginUrl.searchParams.set("callbackUrl", pathname); return NextResponse.redirect(loginUrl); } // 已登录用户访问 /login 或 /register 时重定向到首页 if (token && ["/login", "/register"].includes(pathname)) { return NextResponse.redirect(new URL("/", externalBase)); } return NextResponse.next(); } export const config = { matcher: [ /* * 匹配所有请求路径,除了: * - api/auth (NextAuth.js) * - _next/static (静态文件) * - _next/image (图片优化文件) * - favicon.ico (favicon文件) * - public 文件夹中的文件 */ "/((?!api/auth|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", ], };