202 lines
4.7 KiB
JavaScript
202 lines
4.7 KiB
JavaScript
/**
|
|
* 认证中间件
|
|
* 通过外部 API 验证 Token
|
|
*/
|
|
|
|
import fetch from 'node-fetch';
|
|
import { prisma } from '../db/client.js';
|
|
|
|
// 是否禁用认证(开发/测试模式)
|
|
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 分钟
|
|
|
|
// 测试用户(认证禁用时使用)
|
|
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) {
|
|
// 认证禁用模式(开发/测试)
|
|
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
|
|
}; |