import { NextRequest, NextResponse } from 'next/server' function decodeJwtPayload(token: string): Record | null { try { const parts = token.split('.') if (parts.length !== 3) return null let payload = parts[1].replace(/-/g, '+').replace(/_/g, '/') while (payload.length % 4) payload += '=' return JSON.parse(atob(payload)) } catch { return null } } function isValidPayload(payload: Record | null): boolean { if (!payload) return false return !(payload.exp && (payload.exp as number) < Math.floor(Date.now() / 1000)) } // API Key 验证:检查 ALLOWED_API_KEYS 环境变量(逗号分隔明文 key) // 注意:middleware 运行在 Edge Runtime,不能使用 better-sqlite3 等 Node.js 原生模块 // DB 级别的 key 验证在 route handler 中进行(auth.ts verifyApiKey) function verifyApiKey(key: string): boolean { if (!key.startsWith('ak_')) return false const allowedKeys = process.env.ALLOWED_API_KEYS || '' if (!allowedKeys) return false return allowedKeys.split(',').map(k => k.trim()).includes(key) } function buildLoginRedirect(request: NextRequest) { const { pathname } = request.nextUrl const loginUrl = new URL('/login', request.url) const dest = pathname + (request.nextUrl.search || '') loginUrl.searchParams.set('redirect', dest) return NextResponse.redirect(loginUrl) } export function middleware(request: NextRequest) { const { pathname } = request.nextUrl // 登录/退出路径 + 内部 API 放行(自有 key 认证) if (pathname.startsWith('/login') || pathname === '/' || pathname === '/api/auth/login' || pathname === '/api/auth/logout' || pathname.startsWith('/api/internal/')) { return NextResponse.next() } // API Key 认证(外部系统调用) const authHeader = request.headers.get('authorization') if (authHeader?.startsWith('Bearer ak_')) { const key = authHeader.slice(7) if (verifyApiKey(key)) return NextResponse.next() // 无效 key:API 路由返回 401 if (pathname.startsWith('/api/')) { return NextResponse.json({ error: '未授权' }, { status: 401 }) } } // 优先检查 tlyq_session(共享 JWT) const sharedToken = request.cookies.get('tlyq_session')?.value const sharedPayload = sharedToken ? decodeJwtPayload(sharedToken) : null if (isValidPayload(sharedPayload)) { const response = pathname.startsWith('/api/') ? NextResponse.next() : NextResponse.next() response.cookies.set('session', JSON.stringify({ username: sharedPayload.username }), { httpOnly: true, sameSite: 'lax', path: '/', }) return response } // 回退 session_issue(本地 JWT) const localToken = request.cookies.get('session_issue')?.value const localPayload = localToken ? decodeJwtPayload(localToken) : null if (pathname.startsWith('/api/')) { if (!isValidPayload(localPayload)) { return NextResponse.json({ error: '未登录' }, { status: 401 }) } return NextResponse.next() } if (!isValidPayload(localPayload)) { const response = buildLoginRedirect(request) if (localToken) response.cookies.delete('session_issue') return response } const response = NextResponse.next() response.cookies.set('session', JSON.stringify({ username: localPayload.username }), { httpOnly: true, sameSite: 'lax', path: '/', }) return response } export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'], }