122 lines
4.0 KiB
TypeScript
122 lines
4.0 KiB
TypeScript
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)$).*)',
|
||
],
|
||
}
|
||
|