Files
MoonTVPlus/src/middleware.ts

166 lines
4.8 KiB
TypeScript
Raw Normal View History

2025-08-12 21:50:58 +08:00
/* eslint-disable no-console */
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
2026-01-24 15:31:13 +08:00
import { TOKEN_CONFIG } from '@/lib/refresh-token';
2025-08-12 21:50:58 +08:00
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';
2026-01-18 11:57:05 +08:00
if (!process.env.PASSWORD) {
// 如果未配置密码,重定向到警告页面
2025-08-12 21:50:58 +08:00
const warningUrl = new URL('/warning', request.url);
2026-01-18 11:57:05 +08:00
return warningUrl.pathname === pathname ? NextResponse.next() : NextResponse.redirect(warningUrl);
2025-08-12 21:50:58 +08:00
}
// 从cookie获取认证信息
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo) {
return handleAuthFailure(request, pathname);
}
// localstorage模式在middleware中完成验证
if (storageType === 'localstorage') {
if (!authInfo.password || authInfo.password !== process.env.PASSWORD) {
return handleAuthFailure(request, pathname);
}
return NextResponse.next();
}
2026-01-24 15:31:13 +08:00
// 其他模式:验证签名和时间戳,支持自动续期
2025-08-12 21:50:58 +08:00
// 检查是否有用户名非localStorage模式下密码不存储在cookie中
2026-01-25 17:23:49 +08:00
if (!authInfo.username || !authInfo.role || !authInfo.signature || !authInfo.timestamp) {
2025-08-12 21:50:58 +08:00
return handleAuthFailure(request, pathname);
}
2026-01-24 16:59:33 +08:00
// 强制要求新版 Cookie必须包含 tokenId 和 refreshToken
if (!authInfo.tokenId || !authInfo.refreshToken || !authInfo.refreshExpires) {
console.log(`Old cookie format detected for ${authInfo.username}, forcing re-login`);
return handleAuthFailure(request, pathname);
}
2026-01-24 15:31:13 +08:00
// 验证 Access Token 时间戳
const ACCESS_TOKEN_AGE = TOKEN_CONFIG.ACCESS_TOKEN_AGE;
const now = Date.now();
const age = now - authInfo.timestamp;
2026-01-27 09:08:44 +08:00
// Access Token 已过期,前端负责刷新
2026-01-24 15:31:13 +08:00
if (age > ACCESS_TOKEN_AGE) {
2026-01-27 09:08:44 +08:00
console.log(`Access token expired for ${authInfo.username}, redirecting to login`);
2026-01-24 15:31:13 +08:00
return handleAuthFailure(request, pathname);
}
2025-08-12 21:50:58 +08:00
2026-01-24 15:31:13 +08:00
// Access Token 未过期,验证签名
const isValidSignature = await verifySignature(
authInfo.username,
authInfo.role,
authInfo.timestamp,
authInfo.signature,
process.env.PASSWORD || ''
);
if (!isValidSignature) {
return handleAuthFailure(request, pathname);
}
2026-01-27 09:08:44 +08:00
// 签名验证通过
// 注意Token 续期由前端负责Middleware 不再自动刷新
2026-01-24 15:31:13 +08:00
return NextResponse.next();
2025-08-12 21:50:58 +08:00
}
// 验证签名
async function verifySignature(
2026-01-24 15:31:13 +08:00
username: string,
role: string,
timestamp: number,
2025-08-12 21:50:58 +08:00
signature: string,
secret: string
): Promise<boolean> {
const encoder = new TextEncoder();
const keyData = encoder.encode(secret);
2026-01-24 15:31:13 +08:00
// 构造与生成签名时相同的数据结构
const dataToSign = JSON.stringify({
username,
role,
timestamp
});
const messageData = encoder.encode(dataToSign);
2025-08-12 21:50:58 +08:00
try {
// 导入密钥
const key = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
// 将十六进制字符串转换为Uint8Array
const signatureBuffer = new Uint8Array(
signature.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) || []
);
// 验证签名
return await crypto.subtle.verify(
'HMAC',
key,
signatureBuffer,
messageData
);
} catch (error) {
console.error('签名验证失败:', error);
return false;
}
}
// 处理认证失败的情况
function handleAuthFailure(
request: NextRequest,
pathname: string
): NextResponse {
// 如果是 API 路由,返回 401 状态码
if (pathname.startsWith('/api')) {
return new NextResponse('Unauthorized', { status: 401 });
}
// 否则重定向到登录页面
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 = [
'/_next',
'/favicon.ico',
'/robots.txt',
'/manifest.json',
'/icons/',
'/logo.png',
'/screenshot.png',
];
return skipPaths.some((path) => pathname.startsWith(path));
}
// 配置middleware匹配规则
export const config = {
matcher: [
2026-01-27 00:25:02 +08:00
'/((?!_next/static|_next/image|favicon.ico|login|register|oidc-register|warning|api/login|api/register|api/logout|api/auth/oidc|api/auth/refresh|api/cron/|api/server-config|api/proxy-m3u8|api/cms-proxy|api/tvbox/subscribe|api/theme/css|api/openlist/cms-proxy|api/openlist/play|api/emby/cms-proxy|api/emby/play|api/emby/sources).*)',
2025-08-12 21:50:58 +08:00
],
};