- 在editStorageForm中初始化oss_storage_quota_value和oss_quota_unit - 删除重复的旧配额说明块,保留新的当前配额设置显示 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
315 lines
10 KiB
JavaScript
315 lines
10 KiB
JavaScript
const jwt = require('jsonwebtoken');
|
||
const crypto = require('crypto');
|
||
const { UserDB } = require('./database');
|
||
const { decryptSecret } = require('./utils/encryption');
|
||
|
||
// JWT密钥(必须在环境变量中设置)
|
||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
||
// Refresh Token密钥(使用不同的密钥)
|
||
const REFRESH_SECRET = process.env.REFRESH_SECRET || JWT_SECRET + '-refresh';
|
||
|
||
// Token有效期配置
|
||
const ACCESS_TOKEN_EXPIRES = '2h'; // Access token 2小时
|
||
const REFRESH_TOKEN_EXPIRES = '7d'; // Refresh token 7天
|
||
|
||
// 安全检查:验证JWT密钥配置
|
||
const DEFAULT_SECRETS = [
|
||
'your-secret-key-change-in-production',
|
||
'your-secret-key-change-in-production-PLEASE-CHANGE-THIS'
|
||
];
|
||
|
||
// 安全修复:增强 JWT_SECRET 验证逻辑
|
||
if (DEFAULT_SECRETS.includes(JWT_SECRET)) {
|
||
const errorMsg = `
|
||
╔═══════════════════════════════════════════════════════════════╗
|
||
║ ⚠️ 安全警告 ⚠️ ║
|
||
╠═══════════════════════════════════════════════════════════════╣
|
||
║ JWT_SECRET 使用默认值,存在严重安全风险! ║
|
||
║ ║
|
||
║ 请立即设置环境变量 JWT_SECRET ║
|
||
║ 生成随机密钥: ║
|
||
║ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||
║ ║
|
||
║ 在 backend/.env 文件中设置: ║
|
||
║ JWT_SECRET=你生成的随机密钥 ║
|
||
╚═══════════════════════════════════════════════════════════════╝
|
||
`;
|
||
|
||
// 安全修复:无论环境如何,使用默认 JWT_SECRET 都拒绝启动
|
||
console.error(errorMsg);
|
||
throw new Error('使用默认 JWT_SECRET 存在严重安全风险,服务无法启动!');
|
||
}
|
||
|
||
// 验证 JWT_SECRET 长度(至少 32 字节/64个十六进制字符)
|
||
if (JWT_SECRET.length < 32) {
|
||
const errorMsg = `
|
||
╔═══════════════════════════════════════════════════════════════╗
|
||
║ ⚠️ 配置错误 ⚠️ ║
|
||
╠═══════════════════════════════════════════════════════════════╣
|
||
║ JWT_SECRET 长度不足! ║
|
||
║ ║
|
||
║ 要求: 至少 32 字节 ║
|
||
║ 当前长度: ${JWT_SECRET.length} 字节 ║
|
||
║ ║
|
||
║ 生成安全的随机密钥: ║
|
||
║ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||
╚═══════════════════════════════════════════════════════════════╝
|
||
`;
|
||
console.error(errorMsg);
|
||
throw new Error('JWT_SECRET 长度不足,服务无法启动!');
|
||
}
|
||
|
||
console.log('[安全] ✓ JWT密钥验证通过');
|
||
|
||
// 生成Access Token(短期)
|
||
function generateToken(user) {
|
||
return jwt.sign(
|
||
{
|
||
id: user.id,
|
||
username: user.username,
|
||
is_admin: user.is_admin,
|
||
type: 'access'
|
||
},
|
||
JWT_SECRET,
|
||
{ expiresIn: ACCESS_TOKEN_EXPIRES }
|
||
);
|
||
}
|
||
|
||
// 生成Refresh Token(长期)
|
||
function generateRefreshToken(user) {
|
||
return jwt.sign(
|
||
{
|
||
id: user.id,
|
||
type: 'refresh',
|
||
// 添加随机标识,使每次生成的refresh token不同
|
||
jti: crypto.randomBytes(16).toString('hex')
|
||
},
|
||
REFRESH_SECRET,
|
||
{ expiresIn: REFRESH_TOKEN_EXPIRES }
|
||
);
|
||
}
|
||
|
||
// 验证Refresh Token并返回新的Access Token
|
||
function refreshAccessToken(refreshToken) {
|
||
try {
|
||
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
|
||
|
||
if (decoded.type !== 'refresh') {
|
||
return { success: false, message: '无效的刷新令牌类型' };
|
||
}
|
||
|
||
const user = UserDB.findById(decoded.id);
|
||
|
||
if (!user) {
|
||
return { success: false, message: '用户不存在' };
|
||
}
|
||
|
||
if (user.is_banned) {
|
||
return { success: false, message: '账号已被封禁' };
|
||
}
|
||
|
||
if (!user.is_active) {
|
||
return { success: false, message: '账号未激活' };
|
||
}
|
||
|
||
// 生成新的access token
|
||
const newAccessToken = generateToken(user);
|
||
|
||
return {
|
||
success: true,
|
||
token: newAccessToken,
|
||
user: {
|
||
id: user.id,
|
||
username: user.username,
|
||
is_admin: user.is_admin
|
||
}
|
||
};
|
||
} catch (error) {
|
||
if (error.name === 'TokenExpiredError') {
|
||
return { success: false, message: '刷新令牌已过期,请重新登录' };
|
||
}
|
||
return { success: false, message: '无效的刷新令牌' };
|
||
}
|
||
}
|
||
|
||
// 验证Token中间件
|
||
function authMiddleware(req, res, next) {
|
||
// 从请求头或HttpOnly Cookie获取token(不再接受URL参数以避免泄露)
|
||
const token = req.headers.authorization?.replace('Bearer ', '') || req.cookies?.token;
|
||
|
||
if (!token) {
|
||
return res.status(401).json({
|
||
success: false,
|
||
message: '未提供认证令牌'
|
||
});
|
||
}
|
||
|
||
try {
|
||
const decoded = jwt.verify(token, JWT_SECRET);
|
||
const user = UserDB.findById(decoded.id);
|
||
|
||
if (!user) {
|
||
return res.status(401).json({
|
||
success: false,
|
||
message: '用户不存在'
|
||
});
|
||
}
|
||
|
||
if (user.is_banned) {
|
||
return res.status(403).json({
|
||
success: false,
|
||
message: '账号已被封禁'
|
||
});
|
||
}
|
||
|
||
if (!user.is_active) {
|
||
return res.status(403).json({
|
||
success: false,
|
||
message: '账号未激活'
|
||
});
|
||
}
|
||
|
||
// 将用户信息附加到请求对象(包含所有存储相关字段)
|
||
req.user = {
|
||
id: user.id,
|
||
username: user.username,
|
||
email: user.email,
|
||
is_admin: user.is_admin,
|
||
// OSS存储字段(v3.0新增)
|
||
has_oss_config: user.has_oss_config || 0,
|
||
oss_provider: user.oss_provider,
|
||
oss_region: user.oss_region,
|
||
oss_access_key_id: user.oss_access_key_id,
|
||
// 安全修复:解密 OSS Access Key Secret(如果存在)
|
||
oss_access_key_secret: user.oss_access_key_secret ? decryptSecret(user.oss_access_key_secret) : null,
|
||
oss_bucket: user.oss_bucket,
|
||
oss_endpoint: user.oss_endpoint,
|
||
// 存储相关字段
|
||
storage_permission: user.storage_permission || 'oss_only',
|
||
current_storage_type: user.current_storage_type || 'oss',
|
||
local_storage_quota: user.local_storage_quota || 1073741824,
|
||
local_storage_used: user.local_storage_used || 0,
|
||
// 主题偏好
|
||
theme_preference: user.theme_preference || null
|
||
};
|
||
|
||
next();
|
||
} catch (error) {
|
||
if (error.name === 'TokenExpiredError') {
|
||
return res.status(401).json({
|
||
success: false,
|
||
message: '令牌已过期'
|
||
});
|
||
}
|
||
|
||
return res.status(401).json({
|
||
success: false,
|
||
message: '无效的令牌'
|
||
});
|
||
}
|
||
}
|
||
|
||
// 管理员权限中间件
|
||
function adminMiddleware(req, res, next) {
|
||
if (!req.user || !req.user.is_admin) {
|
||
return res.status(403).json({
|
||
success: false,
|
||
message: '需要管理员权限'
|
||
});
|
||
}
|
||
next();
|
||
}
|
||
|
||
/**
|
||
* 管理员敏感操作二次验证中间件
|
||
*
|
||
* 要求管理员重新输入密码才能执行敏感操作
|
||
* 防止会话劫持后的非法操作
|
||
*
|
||
* @example
|
||
* app.delete('/api/admin/users/:id',
|
||
* authMiddleware,
|
||
* adminMiddleware,
|
||
* requirePasswordConfirmation,
|
||
* async (req, res) => { ... }
|
||
* );
|
||
*/
|
||
function requirePasswordConfirmation(req, res, next) {
|
||
const { password } = req.body;
|
||
|
||
// 检查是否提供了密码
|
||
if (!password) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '执行此操作需要验证密码',
|
||
require_password: true
|
||
});
|
||
}
|
||
|
||
// 验证密码长度(防止空密码)
|
||
if (password.length < 6) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
message: '密码格式错误'
|
||
});
|
||
}
|
||
|
||
// 从数据库重新获取用户信息(不依赖 req.user 中的数据)
|
||
const user = UserDB.findById(req.user.id);
|
||
|
||
if (!user) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
message: '用户不存在'
|
||
});
|
||
}
|
||
|
||
// 验证密码
|
||
const isPasswordValid = UserDB.verifyPassword(password, user.password);
|
||
|
||
if (!isPasswordValid) {
|
||
// 记录安全日志:密码验证失败
|
||
SystemLogDB = require('./database').SystemLogDB;
|
||
SystemLogDB.log({
|
||
level: SystemLogDB.LEVELS.WARN,
|
||
category: SystemLogDB.CATEGORIES.SECURITY,
|
||
action: 'admin_password_verification_failed',
|
||
message: '管理员敏感操作密码验证失败',
|
||
userId: req.user.id,
|
||
username: req.user.username,
|
||
ipAddress: req.ip,
|
||
userAgent: req.get('user-agent'),
|
||
details: {
|
||
endpoint: req.path,
|
||
method: req.method
|
||
}
|
||
});
|
||
|
||
return res.status(403).json({
|
||
success: false,
|
||
message: '密码验证失败,操作已拒绝'
|
||
});
|
||
}
|
||
|
||
// 密码验证成功,继续执行
|
||
next();
|
||
}
|
||
|
||
// 检查JWT密钥是否安全
|
||
function isJwtSecretSecure() {
|
||
return !DEFAULT_SECRETS.includes(JWT_SECRET) && JWT_SECRET.length >= 32;
|
||
}
|
||
|
||
module.exports = {
|
||
JWT_SECRET,
|
||
generateToken,
|
||
generateRefreshToken,
|
||
refreshAccessToken,
|
||
authMiddleware,
|
||
adminMiddleware,
|
||
requirePasswordConfirmation, // 导出二次验证中间件
|
||
isJwtSecretSecure,
|
||
ACCESS_TOKEN_EXPIRES,
|
||
REFRESH_TOKEN_EXPIRES
|
||
};
|