59 lines
2.1 KiB
TypeScript
59 lines
2.1 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
||
import { verifyToken } from '@/lib/jwt'
|
||
|
||
function verifyApiKey(key: string): boolean {
|
||
// API Key 以环境变量形式存储,支持多个 Key(逗号分隔)
|
||
const allowedKeys = process.env.ALLOWED_API_KEYS || ''
|
||
if (!allowedKeys) return false
|
||
const keys = allowedKeys.split(',').map(k => k.trim())
|
||
return keys.includes(key)
|
||
}
|
||
|
||
export async function middleware(request: NextRequest) {
|
||
const { pathname } = request.nextUrl
|
||
if (pathname.startsWith('/login') || pathname === '/') return NextResponse.next()
|
||
if (pathname === '/api/auth/login') return NextResponse.next()
|
||
|
||
const authHeader = request.headers.get('authorization')
|
||
|
||
// API Key 认证:Bearer ak_xxx 格式
|
||
if (authHeader?.startsWith('Bearer ak_')) {
|
||
const key = authHeader.slice(7)
|
||
if (verifyApiKey(key)) return NextResponse.next()
|
||
// 环境变量中未匹配,API 路由仍放行(route handler 可查询数据库二次验证)
|
||
if (pathname.startsWith('/api/')) return NextResponse.next()
|
||
}
|
||
|
||
// Cookie 认证
|
||
const token = request.cookies.get('session_issue')?.value
|
||
|
||
// 构建带 redirect 参数的登录 URL
|
||
function buildLoginRedirect() {
|
||
const loginUrl = new URL('/login', request.url)
|
||
const dest = pathname + (request.nextUrl.search || '')
|
||
loginUrl.searchParams.set('redirect', dest)
|
||
const response = NextResponse.redirect(loginUrl)
|
||
if (token) response.cookies.delete('session_issue')
|
||
return response
|
||
}
|
||
|
||
if (pathname.startsWith('/api/')) {
|
||
if (!token) return NextResponse.json({ error: '未登录' }, { status: 401 })
|
||
const valid = await verifyToken(token)
|
||
if (!valid) return NextResponse.json({ error: '会话已过期' }, { status: 401 })
|
||
return NextResponse.next()
|
||
}
|
||
|
||
if (!token) return buildLoginRedirect()
|
||
const valid = await verifyToken(token)
|
||
if (!valid) return buildLoginRedirect()
|
||
|
||
const response = NextResponse.next()
|
||
response.headers.set('x-original-pathname', pathname + (request.nextUrl.search || ''))
|
||
return response
|
||
}
|
||
|
||
export const config = {
|
||
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
|
||
}
|