138 lines
5.6 KiB
TypeScript
138 lines
5.6 KiB
TypeScript
import { NextResponse } from 'next/server'
|
||
import type { NextRequest } from 'next/server'
|
||
import { getToken } from 'next-auth/jwt'
|
||
import {
|
||
getAuthSessionCookieCandidates,
|
||
resolveSecureAuthCookieFromRequest,
|
||
} from '@/lib/auth-cookie'
|
||
|
||
// 注意:不在middleware中启动内存监控
|
||
// middleware使用Edge Runtime,不支持Node.js API
|
||
// 内存监控会在lib/memory-monitor.ts模块加载时自动启动(仅生产环境)
|
||
|
||
export async function middleware(request: NextRequest) {
|
||
const { pathname, origin } = request.nextUrl
|
||
const requestOrigin = request.headers.get('origin')
|
||
|
||
// 处理API路由的CORS
|
||
if (pathname.startsWith('/api/')) {
|
||
// 处理OPTIONS预检请求
|
||
if (request.method === 'OPTIONS') {
|
||
return new NextResponse(null, {
|
||
status: 204,
|
||
headers: {
|
||
'Access-Control-Allow-Credentials': 'true',
|
||
'Access-Control-Allow-Origin': requestOrigin || origin,
|
||
'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',
|
||
},
|
||
})
|
||
}
|
||
|
||
// 为所有API响应添加CORS头
|
||
const response = NextResponse.next()
|
||
response.headers.set('Access-Control-Allow-Credentials', 'true')
|
||
response.headers.set('Access-Control-Allow-Origin', requestOrigin || origin)
|
||
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')
|
||
|
||
return response
|
||
}
|
||
|
||
// 处理需要认证的页面
|
||
if (pathname === '/' || pathname.startsWith('/admin')) {
|
||
const preferredSecureCookie = resolveSecureAuthCookieFromRequest(request)
|
||
const cookieCandidates = getAuthSessionCookieCandidates(preferredSecureCookie)
|
||
let token = null
|
||
|
||
for (const cookieName of cookieCandidates) {
|
||
token = await getToken({
|
||
req: request,
|
||
secret: process.env.NEXTAUTH_SECRET,
|
||
cookieName,
|
||
secureCookie: cookieName.startsWith('__Secure-'),
|
||
})
|
||
|
||
if (token) {
|
||
break
|
||
}
|
||
}
|
||
|
||
if (!token) {
|
||
const url = new URL('/login', request.url)
|
||
const redirectResponse = NextResponse.redirect(url)
|
||
// 为重定向响应也添加缓存控制头
|
||
redirectResponse.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0')
|
||
redirectResponse.headers.set('Pragma', 'no-cache')
|
||
redirectResponse.headers.set('Expires', '0')
|
||
return redirectResponse
|
||
}
|
||
|
||
// 管理员页面需要管理员权限
|
||
if (pathname.startsWith('/admin')) {
|
||
if ((token as any)?.role !== 'ADMIN') {
|
||
const url = new URL('/', request.url)
|
||
const redirectResponse = NextResponse.redirect(url)
|
||
// 为重定向响应也添加缓存控制头
|
||
redirectResponse.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0')
|
||
redirectResponse.headers.set('Pragma', 'no-cache')
|
||
redirectResponse.headers.set('Expires', '0')
|
||
return redirectResponse
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查是否有新版本部署(通过环境变量控制)
|
||
// 只有在部署新版本时才禁用缓存,否则允许浏览器缓存以提高性能
|
||
const disableCache = process.env.DISABLE_CACHE === 'true'
|
||
const deploymentVersion = process.env.DEPLOYMENT_VERSION || process.env.BUILD_ID || ''
|
||
|
||
// 获取客户端存储的版本号(通过 cookie)
|
||
const clientVersion = request.cookies.get('app-version')?.value || ''
|
||
|
||
// 判断是否需要禁用缓存:有新版本部署且客户端版本与服务器版本不同
|
||
const shouldDisableCache = disableCache && deploymentVersion && clientVersion !== deploymentVersion
|
||
|
||
const response = NextResponse.next()
|
||
|
||
// 如果检测到新版本,设置版本号 cookie
|
||
if (shouldDisableCache && deploymentVersion) {
|
||
response.cookies.set('app-version', deploymentVersion, {
|
||
path: '/',
|
||
maxAge: 60 * 60 * 24 * 365, // 1年
|
||
sameSite: 'lax',
|
||
})
|
||
}
|
||
|
||
// 对于 HTML 页面,根据是否有新版本决定缓存策略
|
||
if (pathname === '/' || pathname.startsWith('/admin') || pathname.startsWith('/share') || pathname.startsWith('/edit') || pathname.endsWith('.html')) {
|
||
if (shouldDisableCache) {
|
||
// 有新版本:禁用缓存,强制浏览器获取最新代码
|
||
response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0')
|
||
response.headers.set('Pragma', 'no-cache')
|
||
response.headers.set('Expires', '0')
|
||
} else {
|
||
// 无新版本:允许浏览器缓存,提高性能
|
||
response.headers.set('Cache-Control', 'public, max-age=3600, must-revalidate') // 缓存1小时
|
||
}
|
||
}
|
||
|
||
// 对于 JS/CSS 等静态资源,使用版本号或时间戳来强制更新
|
||
if (pathname.match(/\.(js|css|json|ico|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/)) {
|
||
if (shouldDisableCache) {
|
||
// 有新版本:禁用缓存
|
||
response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0')
|
||
} else {
|
||
// 无新版本:长期缓存(Next.js 会自动添加版本号到文件名)
|
||
response.headers.set('Cache-Control', 'public, max-age=31536000, immutable')
|
||
}
|
||
}
|
||
|
||
return response
|
||
}
|
||
|
||
export const config = {
|
||
matcher: ['/', '/admin/:path*', '/api/:path*'],
|
||
}
|