import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' import { getToken } from 'next-auth/jwt' // 允许的来源列表(内网和公网) - 支持通过环境变量配置,逗号分隔 const ALLOWED_ORIGINS = (() => { const fromEnv = process.env.ALLOWED_ORIGINS || '' const configuredUrls = [ fromEnv, process.env.APP_PUBLIC_URL || '', process.env.NEXTAUTH_URL || '', ] const list = configuredUrls.flatMap(value => value.split(',')).map(s => normalizeOrigin(s)).filter(Boolean) as string[] const defaults = ['http://localhost:3000', 'http://localhost:4320'] return Array.from(new Set([...defaults, ...list])) })() function normalizeOrigin(value?: string | null) { const trimmed = (value || '').trim() if (!trimmed) return null try { const url = new URL(trimmed) return url.origin } catch { return null } } function getAllowedCorsOrigin(requestOrigin: string | null, sameOrigin: string) { if (!requestOrigin) return sameOrigin const normalized = normalizeOrigin(requestOrigin) if (!normalized) return null if (normalized === sameOrigin || ALLOWED_ORIGINS.includes(normalized)) { return normalized } return null } export async function middleware(request: NextRequest) { const { pathname, origin } = request.nextUrl const requestOrigin = request.headers.get('origin') const corsOrigin = getAllowedCorsOrigin(requestOrigin, origin) // 许可证检查移至后端 API 进行,不再在 middleware 中做快速检查 // 原因:1) Cookie 可伪造 2) 对弱网用户友好 3) 真正的安全验证在后端 // 用户首次访问时,前端会调用 /api/license/check 检查许可证状态 // 处理API路由的CORS if (pathname.startsWith('/api/')) { // 处理OPTIONS预检请求 if (request.method === 'OPTIONS') { if (!corsOrigin) { return new NextResponse(null, { status: 403 }) } return new NextResponse(null, { status: 204, headers: { 'Access-Control-Allow-Credentials': 'true', 'Access-Control-Allow-Origin': corsOrigin, 'Access-Control-Allow-Methods': 'GET,DELETE,PATCH,POST,PUT,OPTIONS', 'Access-Control-Allow-Headers': 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Authorization, Cookie', 'Access-Control-Max-Age': '86400', 'Vary': 'Origin', }, }) } // 为所有API响应添加CORS头 const response = NextResponse.next() if (corsOrigin) { response.headers.set('Access-Control-Allow-Credentials', 'true') response.headers.set('Access-Control-Allow-Origin', corsOrigin) response.headers.set('Access-Control-Allow-Methods', 'GET,DELETE,PATCH,POST,PUT,OPTIONS') response.headers.set('Access-Control-Allow-Headers', 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Authorization, Cookie') response.headers.set('Vary', 'Origin') } return response } // 处理需要认证的页面 if (pathname === '/' || pathname.startsWith('/admin')) { const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }) if (!token) { const url = new URL('/login', request.url) return NextResponse.redirect(url) } // 管理员页面需要管理员权限 const requiresAdmin = pathname.startsWith('/admin') if (requiresAdmin) { if (token.role !== 'ADMIN') { const url = new URL('/', request.url) return NextResponse.redirect(url) } } } return NextResponse.next() } export const config = { // 匹配所有页面路由,排除静态资源 matcher: [ /* * 匹配所有路径,除了: * - _next/static (静态文件) * - _next/image (图片优化) * - favicon.ico (网站图标) * - public 目录下的文件 (如 .svg, .png, .jpg 等) */ '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)', ], }