flnxi-Wu/middleware.ts
2026-05-25 17:12:11 +08:00

122 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)$).*)',
],
}