mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-22 02:24:44 +08:00
feat: use middleware to auth
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable no-console,@typescript-eslint/no-explicit-any */
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getConfig } from '@/lib/config';
|
||||
@@ -11,6 +11,51 @@ const STORAGE_TYPE =
|
||||
(process.env.NEXT_PUBLIC_STORAGE_TYPE as string | undefined) ||
|
||||
'localstorage';
|
||||
|
||||
// 生成签名
|
||||
async function generateSignature(
|
||||
data: string,
|
||||
secret: string
|
||||
): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const keyData = encoder.encode(secret);
|
||||
const messageData = encoder.encode(data);
|
||||
|
||||
// 导入密钥
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
keyData,
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
|
||||
// 生成签名
|
||||
const signature = await crypto.subtle.sign('HMAC', key, messageData);
|
||||
|
||||
// 转换为十六进制字符串
|
||||
return Array.from(new Uint8Array(signature))
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
// 生成认证Cookie(带签名)
|
||||
async function generateAuthCookie(
|
||||
username?: string,
|
||||
password?: string
|
||||
): Promise<string> {
|
||||
const authData: any = { password };
|
||||
|
||||
if (username && process.env.PASSWORD) {
|
||||
authData.username = username;
|
||||
// 使用密码作为密钥对用户名进行签名
|
||||
const signature = await generateSignature(username, process.env.PASSWORD);
|
||||
authData.signature = signature;
|
||||
authData.timestamp = Date.now(); // 添加时间戳防重放攻击
|
||||
}
|
||||
|
||||
return encodeURIComponent(JSON.stringify(authData));
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// 本地 / localStorage 模式——仅校验固定密码
|
||||
@@ -18,7 +63,18 @@ export async function POST(req: NextRequest) {
|
||||
const envPassword = process.env.PASSWORD;
|
||||
|
||||
// 未配置 PASSWORD 时直接放行
|
||||
if (!envPassword) return NextResponse.json({ ok: true });
|
||||
if (!envPassword) {
|
||||
const response = NextResponse.json({ ok: true });
|
||||
|
||||
// 清除可能存在的认证cookie
|
||||
response.cookies.set('auth', '', {
|
||||
path: '/',
|
||||
expires: new Date(0),
|
||||
sameSite: 'strict',
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
const { password } = await req.json();
|
||||
if (typeof password !== 'string') {
|
||||
@@ -32,7 +88,19 @@ export async function POST(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
// 验证成功,设置认证cookie
|
||||
const response = NextResponse.json({ ok: true });
|
||||
const cookieValue = await generateAuthCookie(undefined, password);
|
||||
const expires = new Date();
|
||||
expires.setDate(expires.getDate() + 7); // 7天过期
|
||||
|
||||
response.cookies.set('auth', cookieValue, {
|
||||
path: '/',
|
||||
expires,
|
||||
sameSite: 'strict',
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// 数据库 / redis 模式——校验用户名并尝试连接数据库
|
||||
@@ -50,7 +118,19 @@ export async function POST(req: NextRequest) {
|
||||
username === process.env.USERNAME &&
|
||||
password === process.env.PASSWORD
|
||||
) {
|
||||
return NextResponse.json({ ok: true });
|
||||
// 验证成功,设置认证cookie
|
||||
const response = NextResponse.json({ ok: true });
|
||||
const cookieValue = await generateAuthCookie(username, password);
|
||||
const expires = new Date();
|
||||
expires.setDate(expires.getDate() + 7); // 7天过期
|
||||
|
||||
response.cookies.set('auth', cookieValue, {
|
||||
path: '/',
|
||||
expires,
|
||||
sameSite: 'strict',
|
||||
});
|
||||
|
||||
return response;
|
||||
} else if (username === process.env.USERNAME) {
|
||||
return NextResponse.json({ error: '用户名或密码错误' }, { status: 401 });
|
||||
}
|
||||
@@ -71,7 +151,19 @@ export async function POST(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
// 验证成功,设置认证cookie
|
||||
const response = NextResponse.json({ ok: true });
|
||||
const cookieValue = await generateAuthCookie(username, password);
|
||||
const expires = new Date();
|
||||
expires.setDate(expires.getDate() + 7); // 7天过期
|
||||
|
||||
response.cookies.set('auth', cookieValue, {
|
||||
path: '/',
|
||||
expires,
|
||||
sameSite: 'strict',
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (err) {
|
||||
console.error('数据库验证失败', err);
|
||||
return NextResponse.json({ error: '数据库错误' }, { status: 500 });
|
||||
|
||||
16
src/app/api/logout/route.ts
Normal file
16
src/app/api/logout/route.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function POST() {
|
||||
const response = NextResponse.json({ ok: true });
|
||||
|
||||
// 清除认证cookie
|
||||
response.cookies.set('auth', '', {
|
||||
path: '/',
|
||||
expires: new Date(0),
|
||||
sameSite: 'strict',
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable no-console,@typescript-eslint/no-explicit-any */
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getConfig } from '@/lib/config';
|
||||
@@ -11,6 +11,52 @@ const STORAGE_TYPE =
|
||||
(process.env.NEXT_PUBLIC_STORAGE_TYPE as string | undefined) ||
|
||||
'localstorage';
|
||||
|
||||
// 生成签名
|
||||
async function generateSignature(
|
||||
data: string,
|
||||
secret: string
|
||||
): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const keyData = encoder.encode(secret);
|
||||
const messageData = encoder.encode(data);
|
||||
|
||||
// 导入密钥
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
keyData,
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
|
||||
// 生成签名
|
||||
const signature = await crypto.subtle.sign('HMAC', key, messageData);
|
||||
|
||||
// 转换为十六进制字符串
|
||||
return Array.from(new Uint8Array(signature))
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
// 生成认证Cookie(带签名)
|
||||
async function generateAuthCookie(
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<string> {
|
||||
const authData: any = {
|
||||
password,
|
||||
username,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// 使用process.env.PASSWORD作为签名密钥,而不是用户密码
|
||||
const signingKey = process.env.PASSWORD || '';
|
||||
const signature = await generateSignature(username, signingKey);
|
||||
authData.signature = signature;
|
||||
|
||||
return encodeURIComponent(JSON.stringify(authData));
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// localstorage 模式下不支持注册
|
||||
@@ -57,7 +103,19 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
await db.saveAdminConfig(config);
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
// 注册成功,设置认证cookie
|
||||
const response = NextResponse.json({ ok: true });
|
||||
const cookieValue = await generateAuthCookie(username, password);
|
||||
const expires = new Date();
|
||||
expires.setDate(expires.getDate() + 7); // 7天过期
|
||||
|
||||
response.cookies.set('auth', cookieValue, {
|
||||
path: '/',
|
||||
expires,
|
||||
sameSite: 'strict',
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (err) {
|
||||
console.error('数据库注册失败', err);
|
||||
return NextResponse.json({ error: '数据库错误' }, { status: 500 });
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'sweetalert2/dist/sweetalert2.min.css';
|
||||
|
||||
import { getConfig } from '@/lib/config';
|
||||
|
||||
import AuthProvider from '../components/AuthProvider';
|
||||
import { SiteProvider } from '../components/SiteProvider';
|
||||
import { ThemeProvider } from '../components/ThemeProvider';
|
||||
|
||||
@@ -25,11 +24,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
}
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
userScalable: false,
|
||||
viewportFit: 'cover',
|
||||
themeColor: '#000000',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -69,7 +64,7 @@ export default function RootLayout({
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<SiteProvider siteName={siteName} announcement={announcement}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
{children}
|
||||
</SiteProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
|
||||
@@ -46,7 +46,7 @@ function LoginPageClient() {
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
// 保存密码和用户名以供后续请求使用
|
||||
// API 已经设置了认证cookie,这里只保存到localStorage用于向后兼容
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('password', password);
|
||||
if (shouldAskUsername) {
|
||||
@@ -62,12 +62,14 @@ function LoginPageClient() {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
setError(data.error ?? '服务器错误');
|
||||
}
|
||||
} catch (error) {
|
||||
setError('网络错误,请稍后重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 新增:处理注册逻辑
|
||||
// 处理注册逻辑
|
||||
const handleRegister = async () => {
|
||||
setError(null);
|
||||
if (!password || !username) return;
|
||||
@@ -81,16 +83,20 @@ function LoginPageClient() {
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
// API 已经设置了认证cookie,这里只保存到localStorage用于向后兼容
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('password', password);
|
||||
localStorage.setItem('username', username);
|
||||
}
|
||||
|
||||
const redirect = searchParams.get('redirect') || '/';
|
||||
router.replace(redirect);
|
||||
} else {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
setError(data.error ?? '服务器错误');
|
||||
}
|
||||
} catch (error) {
|
||||
setError('网络错误,请稍后重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -182,7 +188,7 @@ function LoginPageClient() {
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<LoginPageClient />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -20,20 +20,25 @@ import { useSite } from '@/components/SiteProvider';
|
||||
import VideoCard from '@/components/VideoCard';
|
||||
|
||||
function HomeClient() {
|
||||
const [activeTab, setActiveTab] = useState('home');
|
||||
const [activeTab, setActiveTab] = useState<'home' | 'favorites'>('home');
|
||||
const [hotMovies, setHotMovies] = useState<DoubanItem[]>([]);
|
||||
const [hotTvShows, setHotTvShows] = useState<DoubanItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { announcement } = useSite();
|
||||
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(() => {
|
||||
// 检查本地存储中是否已记录弹窗显示状态
|
||||
const hasSeenAnnouncement = localStorage.getItem('hasSeenAnnouncement');
|
||||
if (hasSeenAnnouncement !== announcement) {
|
||||
return true;
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false);
|
||||
|
||||
// 检查公告弹窗状态
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined' && announcement) {
|
||||
const hasSeenAnnouncement = localStorage.getItem('hasSeenAnnouncement');
|
||||
if (hasSeenAnnouncement !== announcement) {
|
||||
setShowAnnouncement(true);
|
||||
} else {
|
||||
setShowAnnouncement(Boolean(!hasSeenAnnouncement && announcement));
|
||||
}
|
||||
}
|
||||
return !hasSeenAnnouncement && announcement; // 未记录且有公告时显示弹窗
|
||||
});
|
||||
}, [announcement]);
|
||||
|
||||
// 收藏夹数据
|
||||
type FavoriteItem = {
|
||||
@@ -129,7 +134,7 @@ function HomeClient() {
|
||||
{ label: '收藏夹', value: 'favorites' },
|
||||
]}
|
||||
active={activeTab}
|
||||
onChange={setActiveTab}
|
||||
onChange={(value) => setActiveTab(value as 'home' | 'favorites')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { ThemeToggle } from '@/components/ThemeToggle';
|
||||
|
||||
import { useSite } from './SiteProvider';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function AuthProvider({ children }: Props) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||
const { siteName } = useSite();
|
||||
const authenticate = useCallback(async () => {
|
||||
// 登录页
|
||||
if (pathname.startsWith('/login') || pathname.startsWith('/api')) {
|
||||
setIsAuthenticated(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 从localStorage获取密码和用户名
|
||||
const password = localStorage.getItem('password');
|
||||
const username = localStorage.getItem('username');
|
||||
const fullPath =
|
||||
typeof window !== 'undefined'
|
||||
? window.location.pathname + window.location.search
|
||||
: pathname;
|
||||
|
||||
// 尝试认证
|
||||
try {
|
||||
const res = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password, username }),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('认证失败');
|
||||
|
||||
setIsAuthenticated(true);
|
||||
} catch (error) {
|
||||
// 认证失败,清理并跳转登录
|
||||
setIsAuthenticated(false);
|
||||
localStorage.removeItem('password');
|
||||
localStorage.removeItem('username');
|
||||
router.replace(`/login?redirect=${encodeURIComponent(fullPath)}`);
|
||||
}
|
||||
}, [pathname, router]);
|
||||
|
||||
useEffect(() => {
|
||||
authenticate();
|
||||
}, [pathname, authenticate]);
|
||||
|
||||
// 认证状态未知时显示加载状态
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className='flex items-center justify-center min-h-screen bg-transparent'>
|
||||
<div className='absolute top-4 right-4'>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<div className='text-center max-w-md mx-auto px-6'>
|
||||
{/* 动画认证图标 */}
|
||||
<div className='relative mb-8'>
|
||||
<div className='relative mx-auto w-24 h-24 bg-gradient-to-r from-green-500 to-emerald-600 rounded-2xl shadow-2xl flex items-center justify-center transform hover:scale-105 transition-transform duration-300'>
|
||||
<div className='text-white text-4xl'>🔐</div>
|
||||
{/* 旋转光环 */}
|
||||
<div className='absolute -inset-2 bg-gradient-to-r from-green-500 to-emerald-600 rounded-2xl opacity-20 animate-spin'></div>
|
||||
</div>
|
||||
|
||||
{/* 浮动粒子效果 */}
|
||||
<div className='absolute top-0 left-0 w-full h-full pointer-events-none'>
|
||||
<div className='absolute top-2 left-2 w-2 h-2 bg-green-400 rounded-full animate-bounce'></div>
|
||||
<div
|
||||
className='absolute top-4 right-4 w-1.5 h-1.5 bg-emerald-400 rounded-full animate-bounce'
|
||||
style={{ animationDelay: '0.5s' }}
|
||||
></div>
|
||||
<div
|
||||
className='absolute bottom-3 left-6 w-1 h-1 bg-lime-400 rounded-full animate-bounce'
|
||||
style={{ animationDelay: '1s' }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 品牌标题 */}
|
||||
<h1 className='text-green-600 tracking-tight text-center text-3xl font-extrabold mb-8 bg-clip-text drop-shadow-sm'>
|
||||
{siteName}
|
||||
</h1>
|
||||
|
||||
{/* 加载消息 */}
|
||||
<div className='space-y-2'>
|
||||
<p className='text-xl font-semibold text-gray-800 dark:text-gray-200 animate-pulse'>
|
||||
正在验证您的身份...
|
||||
</p>
|
||||
<p className='text-sm text-gray-500 dark:text-gray-400'>
|
||||
请稍候,马上就好
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <>{children}</>;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,34 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable no-console */
|
||||
|
||||
'use client';
|
||||
|
||||
import { LogOut } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
/**
|
||||
* 退出登录按钮
|
||||
*
|
||||
* 功能:
|
||||
* 1. 清除 localStorage 中保存的 username 和 password
|
||||
* 2. 跳转到 /login 页面
|
||||
*/
|
||||
export function LogoutButton() {
|
||||
const handleLogout = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('password');
|
||||
export const LogoutButton: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleLogout = async () => {
|
||||
if (loading) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// 调用注销API来清除cookie
|
||||
await fetch('/api/logout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('注销请求失败:', error);
|
||||
}
|
||||
// 使用 replace,避免用户返回上一页时仍然处于已登录状态
|
||||
|
||||
// 清除localStorage中的认证信息(向后兼容)
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('password');
|
||||
localStorage.removeItem('username');
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
@@ -30,4 +41,4 @@ export function LogoutButton() {
|
||||
<LogOut className='w-full h-full' />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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