feat: use middleware to auth

This commit is contained in:
shinya
2025-07-09 13:47:23 +08:00
parent e20dc8f4df
commit 10b8f80aea
9 changed files with 379 additions and 149 deletions

155
src/middleware.ts Normal file
View File

@@ -0,0 +1,155 @@
/* eslint-disable no-console */
import { NextRequest, NextResponse } from 'next/server';
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 跳过不需要认证的路径
if (shouldSkipAuth(pathname)) {
return NextResponse.next();
}
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
// 如果没有设置密码,直接放行
if (storageType === 'localstorage' && !process.env.PASSWORD) {
return NextResponse.next();
}
// 从cookie获取认证信息
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo) {
return redirectToLogin(request, pathname);
}
// localstorage模式在middleware中完成验证
if (storageType === 'localstorage') {
if (!authInfo.password || authInfo.password !== process.env.PASSWORD) {
return redirectToLogin(request, pathname);
}
return NextResponse.next();
}
// 其他模式:只验证签名
// 检查是否有用户名和密码
if (!authInfo.username || !authInfo.password) {
return redirectToLogin(request, pathname);
}
// 验证签名(如果存在)
if (authInfo.signature) {
const isValidSignature = await verifySignature(
authInfo.username,
authInfo.signature,
process.env.PASSWORD || ''
);
// 签名验证通过即可
if (isValidSignature) {
return NextResponse.next();
}
}
// 签名验证失败或不存在签名
return redirectToLogin(request, pathname);
}
// 验证签名
async function verifySignature(
data: string,
signature: string,
secret: string
): Promise<boolean> {
const encoder = new TextEncoder();
const keyData = encoder.encode(secret);
const messageData = encoder.encode(data);
try {
// 导入密钥
const key = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
// 将十六进制字符串转换为ArrayBuffer
const signatureBuffer = new Uint8Array(
signature.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) || []
).buffer;
// 验证签名
return await crypto.subtle.verify(
'HMAC',
key,
signatureBuffer,
messageData
);
} catch (error) {
console.error('签名验证失败:', error);
return false;
}
}
// 从cookie获取认证信息
function getAuthInfoFromCookie(request: NextRequest): {
password?: string;
username?: string;
signature?: string;
timestamp?: number;
} | null {
const authCookie = request.cookies.get('auth');
if (!authCookie) {
return null;
}
try {
const decoded = decodeURIComponent(authCookie.value);
const authData = JSON.parse(decoded);
return authData;
} catch (error) {
return null;
}
}
// 重定向到登录页面
function redirectToLogin(request: NextRequest, pathname: string): NextResponse {
const loginUrl = new URL('/login', request.url);
// 保留完整的URL包括查询参数
const fullUrl = `${pathname}${request.nextUrl.search}`;
loginUrl.searchParams.set('redirect', fullUrl);
return NextResponse.redirect(loginUrl);
}
// 判断是否需要跳过认证的路径
function shouldSkipAuth(pathname: string): boolean {
const skipPaths = [
'/login',
'/api/login',
'/api/register',
'/api/logout',
'/_next',
'/favicon.ico',
'/robots.txt',
'/manifest.json',
'/icons/',
'/logo.png',
'/screenshot.png',
];
return skipPaths.some((path) => pathname.startsWith(path));
}
// 配置middleware匹配规则
export const config = {
matcher: [
/*
* 匹配所有请求路径,除了静态文件
*/
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};