154 lines
4.4 KiB
JavaScript
154 lines
4.4 KiB
JavaScript
const jwt = require('jsonwebtoken');
|
||
const bcrypt = require('bcryptjs');
|
||
const crypto = require('crypto');
|
||
const prisma = require('./prisma');
|
||
|
||
const JWT_EXPIRES_IN = '7d';
|
||
const ADMIN_JWT_EXPIRES_IN = '7d';
|
||
|
||
// 安全:生产环境必须配置 JWT_SECRET,杜绝硬编码密钥
|
||
const JWT_SECRET = process.env.JWT_SECRET;
|
||
if (!JWT_SECRET) {
|
||
if (process.env.NODE_ENV === 'production') {
|
||
throw new Error(
|
||
'JWT_SECRET 环境变量未设置。请在 .env 文件中配置一个随机密钥(可用 `openssl rand -hex 32` 生成)。'
|
||
);
|
||
}
|
||
console.warn('[安全警告] JWT_SECRET 未设置,使用开发环境临时密钥。请勿在生产环境使用。');
|
||
}
|
||
const SECRET = JWT_SECRET || 'dev_only_insecure_secret_do_not_use_in_production';
|
||
|
||
// Admin JWT 使用独立 secret,防止跨角色伪造
|
||
const ADMIN_JWT_SECRET = process.env.ADMIN_JWT_SECRET || (process.env.NODE_ENV === 'production'
|
||
? null
|
||
: 'dev_only_admin_insecure_secret');
|
||
|
||
// 哈希密码
|
||
function hashPassword(password) {
|
||
return bcrypt.hashSync(password, 10);
|
||
}
|
||
|
||
// 验证密码
|
||
function verifyPassword(password, hash) {
|
||
return bcrypt.compareSync(password, hash);
|
||
}
|
||
|
||
// 生成 JWT
|
||
function signToken(userId) {
|
||
return jwt.sign({ userId }, SECRET, { expiresIn: JWT_EXPIRES_IN });
|
||
}
|
||
|
||
// 验证 JWT 并提取 userId
|
||
function verifyToken(token) {
|
||
try {
|
||
const decoded = jwt.verify(token, SECRET);
|
||
return decoded.userId;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Express 中间件:验证 JWT
|
||
function authMiddleware(req, res, next) {
|
||
const authHeader = req.headers.authorization;
|
||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||
return res.status(401).json({ error: '未登录' });
|
||
}
|
||
const token = authHeader.slice(7);
|
||
const userId = verifyToken(token);
|
||
if (!userId) {
|
||
return res.status(401).json({ error: '登录已过期,请重新登录' });
|
||
}
|
||
req.userId = userId;
|
||
next();
|
||
}
|
||
|
||
// Express 中间件:验证 API Key(用于 Hermes 配置拉取)
|
||
// 支持 JWT token 和 API Key(eak_ 前缀)两种认证方式
|
||
async function apiKeyMiddleware(req, res, next) {
|
||
const authHeader = req.headers.authorization;
|
||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||
return res.status(401).json({ error: '需要认证' });
|
||
}
|
||
const token = authHeader.slice(7);
|
||
|
||
// 如果是 API Key(eak_ 前缀),查数据库验证
|
||
if (token.startsWith('eak_')) {
|
||
try {
|
||
const keyHash = crypto.createHash('sha256').update(token).digest('hex');
|
||
const apiKey = await prisma.apiKey.findUnique({
|
||
where: { keyHash },
|
||
select: { id: true, userId: true },
|
||
});
|
||
if (!apiKey) {
|
||
return res.status(401).json({ error: 'API Key 无效' });
|
||
}
|
||
// 更新最后使用时间(不阻塞请求)
|
||
prisma.apiKey.update({
|
||
where: { id: apiKey.id },
|
||
data: { lastUsedAt: new Date() },
|
||
}).catch((err) => console.warn('更新 API Key lastUsedAt 失败:', err));
|
||
req.userId = apiKey.userId;
|
||
req.authMethod = 'apikey';
|
||
return next();
|
||
} catch (err) {
|
||
console.error('API Key 验证失败:', err);
|
||
return res.status(500).json({ error: '认证失败' });
|
||
}
|
||
}
|
||
|
||
// 否则尝试 JWT 认证
|
||
const userId = verifyToken(token);
|
||
if (!userId) {
|
||
return res.status(401).json({ error: '认证无效' });
|
||
}
|
||
req.userId = userId;
|
||
req.authMethod = 'jwt';
|
||
next();
|
||
}
|
||
|
||
// ===== Admin 认证 =====
|
||
|
||
// 生成 Admin JWT
|
||
function adminSignToken(adminId) {
|
||
return jwt.sign({ adminId, role: 'admin' }, ADMIN_JWT_SECRET, { expiresIn: ADMIN_JWT_EXPIRES_IN });
|
||
}
|
||
|
||
// 验证 Admin JWT
|
||
function adminVerifyToken(token) {
|
||
try {
|
||
const decoded = jwt.verify(token, ADMIN_JWT_SECRET);
|
||
if (decoded.role !== 'admin') return null;
|
||
return decoded.adminId;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Express 中间件:验证 Admin JWT
|
||
function adminAuthMiddleware(req, res, next) {
|
||
const authHeader = req.headers.authorization;
|
||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||
return res.status(401).json({ error: '管理员未登录' });
|
||
}
|
||
const token = authHeader.slice(7);
|
||
const adminId = adminVerifyToken(token);
|
||
if (!adminId) {
|
||
return res.status(401).json({ error: '管理员登录已过期,请重新登录' });
|
||
}
|
||
req.adminId = adminId;
|
||
next();
|
||
}
|
||
|
||
module.exports = {
|
||
hashPassword,
|
||
verifyPassword,
|
||
signToken,
|
||
verifyToken,
|
||
authMiddleware,
|
||
apiKeyMiddleware,
|
||
adminSignToken,
|
||
adminVerifyToken,
|
||
adminAuthMiddleware,
|
||
};
|