2025-12-25 10:48:35 +08:00
|
|
|
const jwt = require('jsonwebtoken');
|
|
|
|
|
const User = require('../models/User');
|
2026-06-12 14:16:29 +08:00
|
|
|
const UserRole = require('../models/UserRole');
|
|
|
|
|
const Role = require('../models/Role');
|
|
|
|
|
const SystemSetting = require('../models/SystemSetting');
|
2026-04-28 10:57:17 +08:00
|
|
|
const logger = require('../utils/logger').module('AuthMiddleware');
|
2025-12-25 10:48:35 +08:00
|
|
|
|
2026-06-12 14:16:29 +08:00
|
|
|
// 维护模式缓存,避免每次请求都查数据库
|
|
|
|
|
let maintenanceModeCache = { value: false, updatedAt: 0 };
|
|
|
|
|
const MAINTENANCE_CACHE_TTL = 30000; // 30秒缓存
|
|
|
|
|
|
2026-02-10 10:56:50 +08:00
|
|
|
function getJwtSecret() {
|
|
|
|
|
const envSecret = process.env.JWT_SECRET;
|
|
|
|
|
|
|
|
|
|
if (process.env.NODE_ENV === 'production') {
|
|
|
|
|
if (!envSecret) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
'[致命错误] 生产环境未设置 JWT_SECRET 环境变量!\n' +
|
2026-03-27 19:12:16 +08:00
|
|
|
'请在服务器环境变量中设置强密钥(至少32位随机字符)。\n' +
|
|
|
|
|
'生成命令(PowerShell):-join ((48..57) + (65..90) + (97..122) | Get-Random -Count 64 | ForEach-Object { [char]$_ })'
|
2026-02-10 10:56:50 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (envSecret.length < 32) {
|
2026-03-27 19:12:16 +08:00
|
|
|
throw new Error(
|
|
|
|
|
'[致命错误] 生产环境 JWT_SECRET 长度必须至少32位!当前长度:' + envSecret.length
|
|
|
|
|
);
|
2026-02-10 10:56:50 +08:00
|
|
|
}
|
|
|
|
|
return envSecret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!envSecret) {
|
2026-03-27 19:12:16 +08:00
|
|
|
throw new Error('[错误] JWT_SECRET 未配置,请检查 initConfig.js 是否正确执行');
|
2026-02-10 10:56:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return envSecret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const JWT_SECRET = getJwtSecret();
|
2025-12-25 10:48:35 +08:00
|
|
|
const TOKEN_EXPIRY = process.env.TOKEN_EXPIRY || '24h';
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
const getBrowserInfo = userAgent => {
|
2025-12-25 10:48:35 +08:00
|
|
|
let device = 'Desktop';
|
|
|
|
|
let browser = 'Unknown';
|
|
|
|
|
let os = 'Unknown';
|
|
|
|
|
|
|
|
|
|
if (/Mobile|Android|iPhone|iPad|iPod/i.test(userAgent)) {
|
|
|
|
|
device = 'Mobile';
|
2026-03-27 19:12:16 +08:00
|
|
|
if (/iPad/i.test(userAgent)) {
|
|
|
|
|
device = 'Tablet';
|
|
|
|
|
}
|
2025-12-25 10:48:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (/Firefox/i.test(userAgent)) {
|
|
|
|
|
browser = 'Firefox';
|
|
|
|
|
} else if (/Chrome/i.test(userAgent)) {
|
|
|
|
|
browser = 'Chrome';
|
|
|
|
|
} else if (/Safari/i.test(userAgent)) {
|
|
|
|
|
browser = 'Safari';
|
|
|
|
|
} else if (/Edge/i.test(userAgent)) {
|
|
|
|
|
browser = 'Edge';
|
|
|
|
|
} else if (/MSIE|Trident/i.test(userAgent)) {
|
|
|
|
|
browser = 'IE';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (/Windows/i.test(userAgent)) {
|
|
|
|
|
os = 'Windows';
|
|
|
|
|
} else if (/Mac OS/i.test(userAgent)) {
|
|
|
|
|
os = 'macOS';
|
|
|
|
|
} else if (/Linux/i.test(userAgent)) {
|
|
|
|
|
os = 'Linux';
|
|
|
|
|
} else if (/Android/i.test(userAgent)) {
|
|
|
|
|
os = 'Android';
|
|
|
|
|
} else if (/iOS|iPhone|iPad|iPod/i.test(userAgent)) {
|
|
|
|
|
os = 'iOS';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { device, browser, os };
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
const generateToken = user => {
|
2025-12-25 10:48:35 +08:00
|
|
|
return jwt.sign(
|
|
|
|
|
{
|
|
|
|
|
userId: user.userId,
|
|
|
|
|
username: user.username,
|
2026-03-27 19:12:16 +08:00
|
|
|
roleId: user.roleId,
|
2025-12-25 10:48:35 +08:00
|
|
|
},
|
|
|
|
|
JWT_SECRET,
|
|
|
|
|
{ expiresIn: TOKEN_EXPIRY }
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
const verifyToken = token => {
|
2025-12-25 10:48:35 +08:00
|
|
|
try {
|
|
|
|
|
return jwt.verify(token, JWT_SECRET);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const authMiddleware = async (req, res, next) => {
|
|
|
|
|
try {
|
2026-03-17 11:19:15 +08:00
|
|
|
let token = null;
|
2026-03-27 19:12:16 +08:00
|
|
|
|
2026-03-17 11:19:15 +08:00
|
|
|
const authHeader = req.headers.authorization;
|
|
|
|
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
|
|
|
token = authHeader.substring(7);
|
|
|
|
|
}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
2026-03-17 11:19:15 +08:00
|
|
|
if (!token && req.query.token) {
|
|
|
|
|
token = req.query.token;
|
|
|
|
|
}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
2026-03-17 11:19:15 +08:00
|
|
|
if (!token) {
|
2025-12-25 10:48:35 +08:00
|
|
|
return res.status(401).json({
|
|
|
|
|
success: false,
|
2026-03-27 19:12:16 +08:00
|
|
|
message: '未提供认证令牌',
|
2025-12-25 10:48:35 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const decoded = verifyToken(token);
|
|
|
|
|
|
|
|
|
|
if (!decoded) {
|
|
|
|
|
return res.status(401).json({
|
|
|
|
|
success: false,
|
2026-03-27 19:12:16 +08:00
|
|
|
message: '令牌无效或已过期',
|
2025-12-25 10:48:35 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 11:19:56 +08:00
|
|
|
// 验证用户是否存在,增加详细的错误日志
|
|
|
|
|
let user;
|
|
|
|
|
try {
|
|
|
|
|
user = await User.findByPk(decoded.userId);
|
|
|
|
|
} catch (dbError) {
|
|
|
|
|
// 数据库查询错误
|
2026-04-28 10:57:17 +08:00
|
|
|
logger.error('数据库查询失败', {
|
2026-03-12 11:19:56 +08:00
|
|
|
userId: decoded.userId,
|
|
|
|
|
error: dbError.message,
|
|
|
|
|
stack: dbError.stack,
|
|
|
|
|
});
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
success: false,
|
2026-03-27 19:12:16 +08:00
|
|
|
message: '数据库查询失败,请稍后重试',
|
2026-03-12 11:19:56 +08:00
|
|
|
});
|
|
|
|
|
}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
if (!user) {
|
2026-04-28 10:57:17 +08:00
|
|
|
logger.warn('用户不存在', {
|
2026-03-12 11:19:56 +08:00
|
|
|
userId: decoded.userId,
|
|
|
|
|
username: decoded.username,
|
|
|
|
|
});
|
2025-12-25 10:48:35 +08:00
|
|
|
return res.status(401).json({
|
|
|
|
|
success: false,
|
2026-03-27 19:12:16 +08:00
|
|
|
message: '用户不存在',
|
2025-12-25 10:48:35 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (user.status === 'locked') {
|
2026-04-28 10:57:17 +08:00
|
|
|
logger.warn('账户已被锁定', {
|
2026-03-12 11:19:56 +08:00
|
|
|
userId: user.userId,
|
|
|
|
|
username: user.username,
|
|
|
|
|
});
|
2025-12-25 10:48:35 +08:00
|
|
|
return res.status(403).json({
|
|
|
|
|
success: false,
|
2026-03-27 19:12:16 +08:00
|
|
|
message: '账户已被锁定',
|
2025-12-25 10:48:35 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (user.status === 'inactive') {
|
2026-04-28 10:57:17 +08:00
|
|
|
logger.warn('账户已禁用', {
|
2026-03-12 11:19:56 +08:00
|
|
|
userId: user.userId,
|
|
|
|
|
username: user.username,
|
|
|
|
|
});
|
2025-12-25 10:48:35 +08:00
|
|
|
return res.status(403).json({
|
|
|
|
|
success: false,
|
2026-03-27 19:12:16 +08:00
|
|
|
message: '账户已禁用',
|
2025-12-25 10:48:35 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 14:16:29 +08:00
|
|
|
// 维护模式检查:普通用户无法访问,管理员不受影响
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
if (now - maintenanceModeCache.updatedAt > MAINTENANCE_CACHE_TTL) {
|
|
|
|
|
try {
|
|
|
|
|
const maintenanceSetting = await SystemSetting.findByPk('maintenance_mode');
|
|
|
|
|
maintenanceModeCache = {
|
|
|
|
|
value: maintenanceSetting ? JSON.parse(maintenanceSetting.settingValue) : false,
|
|
|
|
|
updatedAt: now,
|
|
|
|
|
};
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.warn('读取维护模式设置失败', { error: err.message });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (maintenanceModeCache.value) {
|
|
|
|
|
// 查询用户角色,判断是否为管理员
|
|
|
|
|
const userRole = await UserRole.findOne({
|
|
|
|
|
where: { UserId: user.userId },
|
|
|
|
|
include: [{ model: Role }],
|
|
|
|
|
});
|
|
|
|
|
const isAdmin = userRole && userRole.Role && userRole.Role.roleCode === 'admin';
|
|
|
|
|
|
|
|
|
|
if (!isAdmin) {
|
|
|
|
|
logger.info('维护模式:拒绝普通用户访问', {
|
|
|
|
|
userId: user.userId,
|
|
|
|
|
username: user.username,
|
|
|
|
|
});
|
|
|
|
|
return res.status(503).json({
|
|
|
|
|
success: false,
|
|
|
|
|
code: 'MAINTENANCE_MODE',
|
|
|
|
|
message: '系统维护中,请稍后再试',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
req.user = decoded;
|
|
|
|
|
req.userModel = user;
|
|
|
|
|
next();
|
|
|
|
|
} catch (error) {
|
2026-03-12 11:19:56 +08:00
|
|
|
// 捕获未预期的错误
|
2026-04-28 10:57:17 +08:00
|
|
|
logger.error('未预期的错误', {
|
2026-03-12 11:19:56 +08:00
|
|
|
error: error.message,
|
|
|
|
|
stack: error.stack,
|
|
|
|
|
url: req?.url,
|
|
|
|
|
method: req?.method,
|
|
|
|
|
ip: req?.ip,
|
|
|
|
|
});
|
2025-12-25 10:48:35 +08:00
|
|
|
return res.status(500).json({
|
|
|
|
|
success: false,
|
2026-03-27 19:12:16 +08:00
|
|
|
message: '认证失败,请稍后重试',
|
2025-12-25 10:48:35 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const optionalAuth = async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const authHeader = req.headers.authorization;
|
2026-03-27 19:12:16 +08:00
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
|
|
|
const token = authHeader.substring(7);
|
|
|
|
|
const decoded = verifyToken(token);
|
2026-03-27 19:12:16 +08:00
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
if (decoded) {
|
2026-03-12 11:19:56 +08:00
|
|
|
let user;
|
|
|
|
|
try {
|
|
|
|
|
user = await User.findByPk(decoded.userId);
|
|
|
|
|
} catch (dbError) {
|
|
|
|
|
// 数据库错误时不中断请求,仅记录日志
|
2026-04-28 10:57:17 +08:00
|
|
|
logger.warn('可选认证数据库查询失败', {
|
2026-03-12 11:19:56 +08:00
|
|
|
userId: decoded.userId,
|
|
|
|
|
error: dbError.message,
|
|
|
|
|
});
|
|
|
|
|
// 继续执行,不设置用户信息
|
|
|
|
|
next();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
if (user && user.status === 'active') {
|
|
|
|
|
req.user = decoded;
|
|
|
|
|
req.userModel = user;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
next();
|
|
|
|
|
} catch (error) {
|
2026-03-12 11:19:56 +08:00
|
|
|
// 可选认证失败不影响主流程,仅记录日志
|
2026-04-28 10:57:17 +08:00
|
|
|
logger.warn('可选认证失败(已忽略)', {
|
2026-03-12 11:19:56 +08:00
|
|
|
error: error.message,
|
|
|
|
|
url: req?.url,
|
|
|
|
|
});
|
2025-12-25 10:48:35 +08:00
|
|
|
next();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-12 14:16:29 +08:00
|
|
|
/** 清除维护模式缓存,在设置更新时调用 */
|
|
|
|
|
const clearMaintenanceCache = () => {
|
|
|
|
|
maintenanceModeCache = { value: false, updatedAt: 0 };
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-25 10:48:35 +08:00
|
|
|
module.exports = {
|
|
|
|
|
generateToken,
|
|
|
|
|
verifyToken,
|
|
|
|
|
authMiddleware,
|
|
|
|
|
optionalAuth,
|
|
|
|
|
JWT_SECRET,
|
2026-03-27 19:12:16 +08:00
|
|
|
TOKEN_EXPIRY,
|
2026-06-12 14:16:29 +08:00
|
|
|
clearMaintenanceCache,
|
2025-12-25 10:48:35 +08:00
|
|
|
};
|