From 202f159f8f39da433286064924f82eeb525554ce Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 1 Jan 2026 12:32:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Next.js=2016.1.1=20?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E6=80=A7=EF=BC=9A=E5=90=88=E5=B9=B6=20middle?= =?UTF-8?q?ware.ts=20=E5=88=B0=20proxy.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next.js 16.1.1 不再允许同时存在 middleware.ts 和 proxy.ts 文件。 已将 middleware.ts 的所有安全功能(IP黑名单、速率限制、可疑请求检测等)合并到 proxy.ts 中,并删除 middleware.ts。 此更改不影响功能,只是符合 Next.js 新版本的架构要求。 --- proxy.ts | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 8 deletions(-) diff --git a/proxy.ts b/proxy.ts index e06c65e9d..c2333e5e5 100644 --- a/proxy.ts +++ b/proxy.ts @@ -1,17 +1,79 @@ import { type NextRequest, NextResponse } from "next/server"; import { getToken } from "next-auth/jwt"; +import { isBlockedIP, getClientIP } from "@/lib/security/ip-blocklist"; +import { checkRateLimit, rateLimiters } from "@/lib/security/rate-limiter"; +import { isSuspiciousRequest, logSuspiciousRequest } from "@/lib/security/request-monitor"; export async function proxy(request: NextRequest) { - const { pathname, search } = request.nextUrl; + const { pathname } = request.nextUrl; - if (pathname.startsWith("/ping")) { - return new Response("pong", { status: 200 }); + // 🛡️ 安全检查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.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(); + } + + // 允许访问钉钉回调页面(public 文件夹中的文件) + if (pathname === "/dingtalk-callback.html") { + return NextResponse.next(); + } + // 检测是否为 HTTPS(通过 AUTH_URL 或请求协议判断) const isSecure = process.env.AUTH_URL?.startsWith("https://") || request.nextUrl.protocol === "https:"; @@ -28,7 +90,18 @@ export async function proxy(request: NextRequest) { // 未登录用户只能访问 /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); } @@ -42,10 +115,14 @@ export async function proxy(request: NextRequest) { export const config = { matcher: [ - "/", - "/chat/:id", - "/api/:path*", - "/login", - "/register", + /* + * 匹配所有请求路径,除了: + * - 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)$).*)", ], };