shared/lib/auth/middleware.ts

170 lines
6.1 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.

// shared/lib/auth/middleware.ts — 通用 middleware 工厂Edge Runtime 兼容)
// 不使用 Node.js crypto仅用 atob 解码 JWT payload
import { NextResponse, type NextRequest } from 'next/server'
export interface MiddlewareConfig {
/** 公开路径(不需要认证),默认 ['/login', '/api/auth', '/api/health', '/_next', '/favicon.ico'] */
publicPaths?: string[]
/** 需要 admin 权限的路径前缀,默认 ['/admin'] */
adminPaths?: string[]
/** 本地 cookie 名(如 'session_issue'),用于回退验证 */
localCookieName?: string
/** 启用 API Key 认证(读取 ALLOWED_API_KEYS 环境变量),默认 false */
enableApiKey?: boolean
/** 管理员角色名,默认 'admin' */
adminRole?: string
}
// Edge Runtime 兼容:用 atob 解码 JWT payload不验证签名
function decodeJwtPayload(token: string): Record<string, unknown> | 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<string, unknown> | null): boolean {
if (!payload) return false
return !(payload.exp && (payload.exp as number) < Math.floor(Date.now() / 1000))
}
function noCache(response: NextResponse): NextResponse {
response.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate')
return response
}
/**
* 创建通用 middleware
*
* @example
* ```ts
* // src/middleware.ts — 使用默认配置
* export { middleware, config } from '@shared/lib/auth/middleware'
* ```
*
* @example
* ```ts
* // src/middleware.ts — 自定义配置
* import { createMiddleware } from '@shared/lib/auth/middleware'
* export const middleware = createMiddleware({
* localCookieName: 'session_issue',
* adminPaths: ['/settings'],
* enableApiKey: true,
* })
* export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] }
* ```
*/
export function createMiddleware(cfg: MiddlewareConfig = {}) {
const publicPaths = cfg.publicPaths || ['/login', '/api/auth', '/api/health', '/_next', '/favicon.ico']
const adminPaths = cfg.adminPaths || ['/admin']
const localCookieName = cfg.localCookieName
const enableApiKey = cfg.enableApiKey || false
const adminRole = cfg.adminRole || 'admin'
return function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 公开路径放行
if (publicPaths.some(p => pathname.startsWith(p))) {
// 登录页:已登录用户自动跳转首页
if (pathname === '/login') {
const token = request.cookies.get('tlyq_session')?.value
|| (localCookieName ? request.cookies.get(localCookieName)?.value : undefined)
const payload = token ? decodeJwtPayload(token) : null
if (isValidPayload(payload)) {
return NextResponse.redirect(new URL('/', request.url))
}
}
return NextResponse.next()
}
// API Key 认证(可选)
if (enableApiKey) {
const authHeader = request.headers.get('authorization')
if (authHeader?.startsWith('Bearer ak_')) {
const key = authHeader.slice(7)
const allowedKeys = process.env.ALLOWED_API_KEYS || ''
if (allowedKeys && allowedKeys.split(',').map(k => k.trim()).includes(key)) {
return NextResponse.next()
}
if (pathname.startsWith('/api/')) {
return NextResponse.json({ error: '无效的 API Key' }, { status: 401 })
}
}
}
// 验证 tlyq_session共享 JWT
const sharedToken = request.cookies.get('tlyq_session')?.value
const sharedPayload = sharedToken ? decodeJwtPayload(sharedToken) : null
if (isValidPayload(sharedPayload)) {
// 管理路径检查 admin 权限
if (adminPaths.some(p => pathname.startsWith(p))) {
const role = sharedPayload?.role as string
if (role !== adminRole && role !== 'admin') {
return new NextResponse('Forbidden', { status: 403 })
}
}
// 设置 session cookie兼容旧代码
if (sharedPayload) {
const response = pathname.startsWith('/api/') ? NextResponse.next() : noCache(NextResponse.next())
response.cookies.set('session', JSON.stringify({ username: sharedPayload.username }), {
httpOnly: true, sameSite: 'lax', path: '/',
})
return response
}
}
// 回退本地 cookie可选
if (localCookieName) {
const localToken = request.cookies.get(localCookieName)?.value
const localPayload = localToken ? decodeJwtPayload(localToken) : null
if (isValidPayload(localPayload)) {
// 管理路径检查 admin 权限
if (adminPaths.some(p => pathname.startsWith(p))) {
const role = localPayload?.role as string
if (role !== adminRole && role !== 'admin') {
return new NextResponse('Forbidden', { status: 403 })
}
}
if (pathname.startsWith('/api/')) {
return NextResponse.next()
}
if (localPayload) {
const response = noCache(NextResponse.next())
response.cookies.set('session', JSON.stringify({ username: localPayload.username }), {
httpOnly: true, sameSite: 'lax', path: '/',
})
return response
}
}
// 清除无效的本地 cookie
if (localToken) {
const loginUrl = new URL('/login', request.url)
const response = NextResponse.redirect(loginUrl)
response.cookies.delete(localCookieName)
if (sharedToken) response.cookies.delete('tlyq_session')
return response
}
}
// 未认证API 返回 401页面重定向登录
if (pathname.startsWith('/api/')) {
return NextResponse.json({ error: '未登录' }, { status: 401 })
}
return noCache(NextResponse.redirect(new URL('/login', request.url)))
}
}
// 默认导出(无配置,适用于简单站点)
export const middleware = createMiddleware()
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] }