Files
novalon-website/src/proxy.ts
T

106 lines
3.3 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 { NextRequest, NextResponse } from 'next/server';
interface JwtPayload {
userId: string;
username: string;
role: string;
exp?: number;
}
// ============ Edge-compatible JWT 验证 ============
// Next.js Proxy(原 Middleware 约定,Next 16 起重命名)运行在 Edge Runtime,
// 不能依赖 jsonwebtoken 的 Node crypto。
// 这里使用 Web Crypto API 实现 HS256 签名验证,与 API 路由中的 jsonwebtoken 共享同一密钥。
function base64urlToBuffer(value: string): Uint8Array {
const base64 = value.replace(/-/g, '+').replace(/_/g, '/').padEnd(value.length + ((4 - (value.length % 4)) % 4), '=');
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function bufferToBase64url(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]!);
}
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
async function verifyAccessTokenEdge(token: string): Promise<JwtPayload> {
const secret = process.env.JWT_SECRET;
if (!secret) {
throw new Error('JWT_SECRET is not configured');
}
const [headerB64, payloadB64, signatureB64] = token.split('.');
if (!headerB64 || !payloadB64 || !signatureB64) {
throw new Error('Malformed token');
}
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(`${headerB64}.${payloadB64}`));
const expectedSignature = bufferToBase64url(signature);
if (expectedSignature !== signatureB64) {
throw new Error('Invalid token signature');
}
const payload = JSON.parse(new TextDecoder().decode(base64urlToBuffer(payloadB64))) as JwtPayload & { exp?: number };
if (payload.exp && payload.exp * 1000 < Date.now()) {
throw new Error('Token expired');
}
return payload;
}
// ============ Proxy(原 Middleware,Next 16 弃用 middleware 约定) ============
function redirectToLogin(request: NextRequest, pathname: string): NextResponse {
const loginUrl = new URL('/admin/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}
export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// 只处理 /admin 路由(除了登录页和 API 路由)
if (pathname.startsWith('/admin') && !pathname.startsWith('/admin/login') && !pathname.startsWith('/api/')) {
const token = request.cookies.get('novalon_token')?.value;
const authHeader = request.headers.get('authorization');
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
const accessToken = token || bearerToken;
// 未提供令牌时重定向到登录页
if (!accessToken) {
return redirectToLogin(request, pathname);
}
// 验证令牌有效性
try {
await verifyAccessTokenEdge(accessToken);
} catch {
return redirectToLogin(request, pathname);
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/admin/:path*'],
};