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

View File

@@ -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 });

View 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;
}

View File

@@ -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 });