paper-burner/local-proxy/auth/middleware.js

205 lines
4.9 KiB
JavaScript

/**
* 认证中间件
* 通过外部 API 验证 Token
*/
import fetch from 'node-fetch';
import { prisma } from '../db/client.js';
// 是否禁用认证(开发/测试模式)
// TODO: 生产环境必须移除此配置,强制要求认证
const AUTH_DISABLED = process.env.AUTH_DISABLED === 'true' || process.env.NODE_ENV === 'test';
// 默认认证 API URL
const DEFAULT_AUTH_CHECK_URL = 'https://sxwz.xueai.art/api/auth/check/checkTokenRn';
// Token 缓存(减少外部 API 调用)
const tokenCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 分钟
// 测试用户(认证禁用时使用)
// TODO: 生产环境必须移除此测试用户
const TEST_USER = {
id: 'test-user-001',
username: 'testuser',
nickname: '测试用户'
};
/**
* 验证 Token
* @param {string} token - JWT Token
* @returns {Promise<Object|null>} 用户信息或 null
*/
export async function verifyToken(token) {
if (!token) return null;
// 1. 检查缓存
const cached = tokenCache.get(token);
if (cached && cached.expiresAt > Date.now()) {
return cached.user;
}
// 2. 调用外部 API 验证
const authCheckUrl = process.env.AUTH_CHECK_URL || DEFAULT_AUTH_CHECK_URL;
const checkUrl = `${authCheckUrl}/${token}`;
try {
const response = await fetch(checkUrl, {
method: 'GET',
headers: {
'Accept': 'application/json',
'User-Agent': 'PaperBurner-LocalProxy/1.0'
},
signal: AbortSignal.timeout(5000) // 5 秒超时
});
if (!response.ok) {
console.warn('[Auth] Token verification failed:', response.status);
return null;
}
const data = await response.json();
// 检查响应格式
if (data.code !== '0' && data.success !== true) {
console.warn('[Auth] Token verification returned error:', data.msg || data.message);
return null;
}
const user = data.data;
if (!user || !user.id) {
console.warn('[Auth] Invalid user data in response');
return null;
}
// 3. 缓存结果
tokenCache.set(token, {
user: {
id: String(user.id),
username: user.username,
nickname: user.nickname || user.username
},
expiresAt: Date.now() + CACHE_TTL
});
return user;
} catch (error) {
console.error('[Auth] Token verification error:', error.message);
return null;
}
}
/**
* 确保用户存在(首次访问时自动创建)
* @param {string} userId - 用户 ID
* @param {string} name - 用户名称
*/
export async function ensureUserExists(userId, name) {
try {
await prisma.user.upsert({
where: { id: userId },
update: {}, // 不更新任何字段
create: {
id: userId,
name: name || `用户${userId.slice(-6)}`
}
});
} catch (error) {
console.error('[Auth] Failed to ensure user exists:', error.message);
}
}
/**
* 认证中间件
* 验证 Token 并自动创建用户
*/
export async function requireAuth(req, res, next) {
// 认证禁用模式(开发/测试)
// TODO: 生产环境必须移除此分支,强制要求认证
if (AUTH_DISABLED) {
await ensureUserExists(TEST_USER.id, TEST_USER.nickname);
req.user = TEST_USER;
if (typeof next === 'function') {
next();
}
return;
}
const authHeader = req.headers.authorization;
const token = authHeader?.split(' ')[1];
if (!token) {
res.writeHead(401, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': req.headers.origin || '*'
});
res.end(JSON.stringify({ error: 'Token required' }));
return;
}
const user = await verifyToken(token);
if (!user) {
res.writeHead(401, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': req.headers.origin || '*'
});
res.end(JSON.stringify({ error: 'Invalid or expired token' }));
return;
}
// 自动创建用户(首次访问)
await ensureUserExists(user.id, user.nickname || user.username);
// 将用户信息附加到请求对象
req.user = user;
// 调用下一个处理器
if (typeof next === 'function') {
next();
}
}
/**
* 可选认证中间件
* 如果有 Token 则验证,没有则跳过
*/
export async function optionalAuth(req, res, next) {
const authHeader = req.headers.authorization;
const token = authHeader?.split(' ')[1];
if (token) {
const user = await verifyToken(token);
if (user) {
await ensureUserExists(user.id, user.nickname || user.username);
req.user = user;
}
}
if (typeof next === 'function') {
next();
}
}
/**
* 清理过期的 Token 缓存
*/
export function cleanupTokenCache() {
const now = Date.now();
for (const [token, data] of tokenCache.entries()) {
if (data.expiresAt < now) {
tokenCache.delete(token);
}
}
}
// 每 10 分钟清理一次缓存
setInterval(cleanupTokenCache, 10 * 60 * 1000);
export default {
verifyToken,
ensureUserExists,
requireAuth,
optionalAuth
};