mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-23 19:52:09 +08:00
feat: use middleware to auth
This commit is contained in:
155
src/middleware.ts
Normal file
155
src/middleware.ts
Normal 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).*)',
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user