修改管理后台

This commit is contained in:
shenquanyi
2025-09-12 20:08:42 +08:00
parent 39d61c6f9b
commit 80a24c2d60
286 changed files with 75316 additions and 9452 deletions

View File

@@ -48,7 +48,7 @@ const checkRole = (roles) => {
const user = await User.findByPk(userId, {
include: [{
model: Role,
as: 'roles', // 添加as属性指定关联别名
as: 'role', // 使用正确的关联别名
attributes: ['name']
}]
});
@@ -61,7 +61,7 @@ const checkRole = (roles) => {
}
// 获取用户角色名称数组
const userRoles = user.roles.map(role => role.name);
const userRoles = user.role ? [user.role.name] : [];
// 检查用户是否具有所需角色
const hasRequiredRole = roles.some(role => userRoles.includes(role));

View File

@@ -0,0 +1,316 @@
/**
* 自动操作日志中间件
* @file autoOperationLogger.js
* @description 自动记录所有API操作日志的中间件
*/
const { OperationLog } = require('../models');
/**
* 自动操作日志中间件
* 自动记录所有经过认证的API操作
*/
const autoOperationLogger = async (req, res, next) => {
const startTime = Date.now();
const originalSend = res.send;
let responseBody = null;
// 拦截响应数据
res.send = function(data) {
responseBody = data;
return originalSend.call(this, data);
};
// 在响应完成后记录日志
res.on('finish', async () => {
try {
// 只记录经过认证的操作
if (!req.user) {
console.log(`[操作日志] 跳过记录 - 无用户信息: ${req.method} ${req.originalUrl}`);
return;
}
const executionTime = Date.now() - startTime;
const operationType = getOperationType(req.method, req.originalUrl);
if (!operationType) {
console.log(`[操作日志] 跳过记录 - 不支持的操作类型: ${req.method} ${req.originalUrl}`);
return; // 不记录的操作类型
}
console.log(`[操作日志] 开始记录: ${req.user.username} ${operationType} ${req.method} ${req.originalUrl}`);
// 获取模块名称
const moduleName = getModuleName(req.originalUrl);
// 获取表名
const tableName = getTableName(req.originalUrl);
// 获取记录ID
const recordId = getRecordId(req);
// 生成操作描述
const operationDesc = generateOperationDesc(req, responseBody);
// 获取操作前后数据
const { oldData, newData } = await getOperationData(req, res, operationType);
// 获取IP地址
const ipAddress = getClientIP(req);
// 记录操作日志
await OperationLog.recordOperation({
userId: req.user.id,
username: req.user.username,
userRole: req.user.role || 'unknown',
operationType,
moduleName,
tableName,
recordId,
operationDesc,
oldData,
newData,
ipAddress,
userAgent: req.get('User-Agent'),
requestUrl: req.originalUrl,
requestMethod: req.method,
responseStatus: res.statusCode,
executionTime,
errorMessage: res.statusCode >= 400 ? (responseBody?.message || '操作失败') : null
});
console.log(`[操作日志] ${req.user.username} ${operationType} ${moduleName} - ${operationDesc}`);
} catch (error) {
console.error('记录操作日志失败:', error);
// 不抛出错误,避免影响主业务
}
});
next();
};
/**
* 根据HTTP方法和URL获取操作类型
*/
const getOperationType = (method, url) => {
const methodUpper = method.toUpperCase();
// 特殊URL处理
if (url.includes('/auth/login')) {
return 'LOGIN';
}
if (url.includes('/auth/logout')) {
return 'LOGOUT';
}
if (url.includes('/export')) {
return 'EXPORT';
}
if (url.includes('/import')) {
return 'IMPORT';
}
if (url.includes('/batch-delete')) {
return 'BATCH_DELETE';
}
if (url.includes('/batch-update')) {
return 'BATCH_UPDATE';
}
// 标准HTTP方法映射
switch (methodUpper) {
case 'POST':
return 'CREATE';
case 'PUT':
case 'PATCH':
return 'UPDATE';
case 'DELETE':
return 'DELETE';
case 'GET':
// GET请求记录所有操作
return 'READ';
default:
return null;
}
};
/**
* 根据URL获取模块名称
*/
const getModuleName = (url) => {
const pathSegments = url.split('/').filter(segment => segment);
if (pathSegments.length < 2) {
return '未知模块';
}
const moduleMap = {
'users': '用户管理',
'farms': '农场管理',
'animals': '动物管理',
'devices': '设备管理',
'alerts': '告警管理',
'products': '产品管理',
'orders': '订单管理',
'auth': '认证管理',
'stats': '统计分析',
'map': '地图服务',
'operation-logs': '操作日志',
'roles': '角色管理',
'permissions': '权限管理',
'menus': '菜单管理',
'cattle-batches': '牛只批次管理',
'cattle-pens': '牛舍管理',
'cattle-transfer-records': '牛只转移记录',
'cattle-exit-records': '牛只出栏记录',
'electronic-fences': '电子围栏',
'electronic-fence-points': '围栏点位',
'pens': '圈舍管理'
};
const moduleKey = pathSegments[1];
return moduleMap[moduleKey] || moduleKey || '未知模块';
};
/**
* 根据URL获取表名
*/
const getTableName = (url) => {
const pathSegments = url.split('/').filter(segment => segment);
if (pathSegments.length < 2) {
return 'unknown';
}
const tableMap = {
'users': 'users',
'farms': 'farms',
'animals': 'animals',
'devices': 'devices',
'alerts': 'alerts',
'products': 'products',
'orders': 'orders',
'cattle-batches': 'cattle_batches',
'cattle-pens': 'cattle_pens',
'cattle-transfer-records': 'cattle_transfer_records',
'cattle-exit-records': 'cattle_exit_records',
'electronic-fences': 'electronic_fences',
'electronic-fence-points': 'electronic_fence_points',
'pens': 'pens',
'roles': 'roles',
'permissions': 'permissions',
'menus': 'menu_permissions'
};
const tableKey = pathSegments[1];
return tableMap[tableKey] || tableKey || 'unknown';
};
/**
* 获取记录ID
*/
const getRecordId = (req) => {
// 从URL参数中获取ID
const id = req.params.id || req.params.farmId || req.params.animalId ||
req.params.deviceId || req.params.alertId || req.params.productId ||
req.params.orderId || req.params.batchId || req.params.penId;
return id ? parseInt(id) : null;
};
/**
* 生成操作描述
*/
const generateOperationDesc = (req, responseBody) => {
const method = req.method.toUpperCase();
const url = req.originalUrl;
const moduleName = getModuleName(url);
// 根据操作类型生成描述
switch (method) {
case 'POST':
if (url.includes('/auth/login')) {
return '用户登录';
}
if (url.includes('/auth/logout')) {
return '用户登出';
}
if (url.includes('/export')) {
return `导出${moduleName}数据`;
}
if (url.includes('/import')) {
return `导入${moduleName}数据`;
}
if (url.includes('/batch-delete')) {
return `批量删除${moduleName}数据`;
}
if (url.includes('/batch-update')) {
return `批量更新${moduleName}数据`;
}
return `新增${moduleName}记录`;
case 'PUT':
case 'PATCH':
return `更新${moduleName}记录`;
case 'DELETE':
if (url.includes('/batch-delete')) {
return `批量删除${moduleName}数据`;
}
return `删除${moduleName}记录`;
case 'GET':
if (url.includes('/stats') || url.includes('/analytics')) {
return `查看${moduleName}统计`;
}
if (url.includes('/reports')) {
return `查看${moduleName}报表`;
}
return `查看${moduleName}数据`;
default:
return `${method}操作${moduleName}`;
}
};
/**
* 获取操作前后数据
*/
const getOperationData = async (req, res, operationType) => {
let oldData = null;
let newData = null;
try {
// 对于更新和删除操作,尝试获取操作前的数据
if (operationType === 'UPDATE' || operationType === 'DELETE') {
// 这里可以根据需要实现获取旧数据的逻辑
// 由于需要查询数据库暂时返回null
oldData = null;
}
// 对于新增和更新操作,获取操作后的数据
if (operationType === 'CREATE' || operationType === 'UPDATE') {
// 从响应体中获取新数据
if (res.responseBody && res.responseBody.data) {
newData = res.responseBody.data;
}
}
} catch (error) {
console.error('获取操作数据失败:', error);
}
return { oldData, newData };
};
/**
* 获取客户端IP地址
*/
const getClientIP = (req) => {
return req.ip ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
(req.connection.socket ? req.connection.socket.remoteAddress : null) ||
req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
'unknown';
};
module.exports = {
autoOperationLogger
};

View File

@@ -0,0 +1,66 @@
/**
* 操作日志权限检查中间件
* @file operationLogAuth.js
* @description 检查用户是否有操作日志访问权限
*/
const { User, Role, Permission } = require('../models');
/**
* 检查操作日志权限的中间件
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
* @param {Function} next - 下一步函数
*/
const checkOperationLogPermission = async (req, res, next) => {
try {
const userId = req.user.id;
// 查询用户及其角色和权限
const user = await User.findByPk(userId, {
include: [{
model: Role,
as: 'role',
include: [{
model: Permission,
as: 'permissions',
through: { attributes: [] },
attributes: ['permission_key']
}]
}]
});
if (!user || !user.role) {
return res.status(403).json({
success: false,
message: '用户角色信息不存在'
});
}
// 获取用户权限列表
const userPermissions = user.role.permissions
? user.role.permissions.map(p => p.permission_key)
: [];
// 检查是否有操作日志查看权限
if (!userPermissions.includes('operation_log:view')) {
return res.status(403).json({
success: false,
message: '权限不足,无法访问操作日志'
});
}
// 将权限信息添加到请求对象中
req.user.permissions = userPermissions;
next();
} catch (error) {
console.error('操作日志权限检查失败:', error);
return res.status(500).json({
success: false,
message: '权限检查失败'
});
}
};
module.exports = {
checkOperationLogPermission
};

View File

@@ -0,0 +1,216 @@
/**
* 操作日志中间件
* @file operationLogger.js
* @description 自动记录用户操作的中间件
*/
const { OperationLog } = require('../models');
/**
* 操作日志中间件
* @param {Object} options 配置选项
* @param {string} options.moduleName 模块名称
* @param {string} options.tableName 数据表名
* @param {Function} options.getRecordId 获取记录ID的函数
* @param {Function} options.getOperationDesc 获取操作描述的函数
* @param {Function} options.getOldData 获取操作前数据的函数(可选)
* @param {Function} options.getNewData 获取操作后数据的函数(可选)
* @returns {Function} 中间件函数
*/
const operationLogger = (options = {}) => {
const {
moduleName,
tableName,
getRecordId = () => null,
getOperationDesc = () => '未知操作',
getOldData = () => null,
getNewData = () => null
} = options;
return async (req, res, next) => {
const startTime = Date.now();
const originalSend = res.send;
let responseBody = null;
// 拦截响应数据
res.send = function(data) {
responseBody = data;
return originalSend.call(this, data);
};
// 在响应完成后记录日志
res.on('finish', async () => {
try {
const executionTime = Date.now() - startTime;
const operationType = getOperationType(req.method);
if (!operationType) {
return; // 只记录增删改操作
}
const recordId = getRecordId(req, res);
const operationDesc = getOperationDesc(req, res);
const oldData = getOldData(req, res);
const newData = getNewData(req, res);
// 获取用户信息
const user = req.user;
if (!user) {
return; // 没有用户信息不记录日志
}
// 获取IP地址
const ipAddress = req.ip ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
(req.connection.socket ? req.connection.socket.remoteAddress : null) ||
req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
'unknown';
// 记录操作日志
await OperationLog.recordOperation({
userId: user.id,
username: user.username,
userRole: user.roles,
operationType,
moduleName,
tableName,
recordId,
operationDesc,
oldData,
newData,
ipAddress,
userAgent: req.get('User-Agent'),
requestUrl: req.originalUrl,
requestMethod: req.method,
responseStatus: res.statusCode,
executionTime,
errorMessage: res.statusCode >= 400 ? responseBody?.message || '操作失败' : null
});
} catch (error) {
console.error('记录操作日志失败:', error);
// 不抛出错误,避免影响主业务
}
});
next();
};
};
/**
* 根据HTTP方法获取操作类型
* @param {string} method HTTP方法
* @returns {string|null} 操作类型
*/
const getOperationType = (method) => {
switch (method.toUpperCase()) {
case 'POST':
return 'CREATE';
case 'PUT':
case 'PATCH':
return 'UPDATE';
case 'DELETE':
return 'DELETE';
default:
return null;
}
};
/**
* 创建操作日志记录器
* @param {Object} options 配置选项
* @returns {Function} 中间件函数
*/
const createOperationLogger = (options) => {
return operationLogger(options);
};
/**
* 批量操作日志记录器
* @param {Array} operations 操作配置数组
* @returns {Function} 中间件函数
*/
const createBatchOperationLogger = (operations) => {
return async (req, res, next) => {
const startTime = Date.now();
const originalSend = res.send;
let responseBody = null;
// 拦截响应数据
res.send = function(data) {
responseBody = data;
return originalSend.call(this, data);
};
// 在响应完成后记录日志
res.on('finish', async () => {
try {
const executionTime = Date.now() - startTime;
const operationType = getOperationType(req.method);
if (!operationType) {
return;
}
const user = req.user;
if (!user) {
return;
}
const ipAddress = req.ip ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
(req.connection.socket ? req.connection.socket.remoteAddress : null) ||
req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
'unknown';
// 为每个操作配置记录日志
for (const operation of operations) {
const {
moduleName,
tableName,
getRecordId = () => null,
getOperationDesc = () => '未知操作',
getOldData = () => null,
getNewData = () => null
} = operation;
const recordId = getRecordId(req, res);
const operationDesc = getOperationDesc(req, res);
const oldData = getOldData(req, res);
const newData = getNewData(req, res);
await OperationLog.recordOperation({
userId: user.id,
username: user.username,
userRole: user.roles,
operationType,
moduleName,
tableName,
recordId,
operationDesc,
oldData,
newData,
ipAddress,
userAgent: req.get('User-Agent'),
requestUrl: req.originalUrl,
requestMethod: req.method,
responseStatus: res.statusCode,
executionTime,
errorMessage: res.statusCode >= 400 ? responseBody?.message || '操作失败' : null
});
}
} catch (error) {
console.error('记录批量操作日志失败:', error);
}
});
next();
};
};
module.exports = {
operationLogger,
createOperationLogger,
createBatchOperationLogger,
getOperationType
};

View File

@@ -0,0 +1,224 @@
/**
* 权限验证中间件
* @file permission.js
* @description 基于权限的访问控制中间件
*/
const { User, Role, Permission } = require('../models');
const { hasPermission } = require('../config/permissions');
/**
* 权限验证中间件
* @param {string|Array} requiredPermissions 需要的权限
* @returns {Function} 中间件函数
*/
const requirePermission = (requiredPermissions) => {
return async (req, res, next) => {
try {
// 检查用户是否已认证
if (!req.user || !req.user.id) {
return res.status(401).json({
success: false,
message: '未授权访问'
});
}
// 获取用户信息(包含角色和权限)
const user = await User.findByPk(req.user.id, {
include: [{
model: Role,
as: 'role',
attributes: ['id', 'name'],
include: [{
model: Permission,
as: 'permissions',
through: { attributes: [] },
attributes: ['permission_key']
}]
}]
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
});
}
// 检查用户状态
if (user.status !== 'active') {
return res.status(403).json({
success: false,
message: '账户已被禁用'
});
}
// 获取用户权限(从数据库)
const userPermissions = user.role && user.role.permissions
? user.role.permissions.map(p => p.permission_key)
: [];
// 检查权限
const hasRequiredPermission = hasPermission(userPermissions, requiredPermissions);
if (!hasRequiredPermission) {
return res.status(403).json({
success: false,
message: '权限不足',
requiredPermissions: Array.isArray(requiredPermissions) ? requiredPermissions : [requiredPermissions],
userPermissions: userPermissions
});
}
// 将用户信息添加到请求对象
req.currentUser = {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
permissions: userPermissions
};
next();
} catch (error) {
console.error('权限验证错误:', error);
res.status(500).json({
success: false,
message: '权限验证失败'
});
}
};
};
/**
* 角色验证中间件
* @param {string|Array} requiredRoles 需要的角色
* @returns {Function} 中间件函数
*/
const requireRole = (requiredRoles) => {
return async (req, res, next) => {
try {
// 检查用户是否已认证
if (!req.user || !req.user.id) {
return res.status(401).json({
success: false,
message: '未授权访问'
});
}
// 获取用户信息(包含角色)
const user = await User.findByPk(req.user.id, {
include: [{
model: Role,
as: 'role',
attributes: ['id', 'name']
}]
});
if (!user || !user.role) {
return res.status(403).json({
success: false,
message: '用户角色不存在'
});
}
// 检查角色
const roles = Array.isArray(requiredRoles) ? requiredRoles : [requiredRoles];
const hasRequiredRole = roles.includes(user.role.name);
if (!hasRequiredRole) {
return res.status(403).json({
success: false,
message: '角色权限不足',
requiredRoles: roles,
userRole: user.role.name
});
}
// 将用户信息添加到请求对象
req.currentUser = {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
permissions: getRolePermissions(user.role.name)
};
next();
} catch (error) {
console.error('角色验证错误:', error);
res.status(500).json({
success: false,
message: '角色验证失败'
});
}
};
};
/**
* 管理员权限中间件
* @returns {Function} 中间件函数
*/
const requireAdmin = () => {
return requireRole('admin');
};
/**
* 养殖场管理员权限中间件
* @returns {Function} 中间件函数
*/
const requireFarmManager = () => {
return requireRole(['admin', 'farm_manager']);
};
/**
* 监管人员权限中间件
* @returns {Function} 中间件函数
*/
const requireInspector = () => {
return requireRole(['admin', 'farm_manager', 'inspector']);
};
/**
* 获取用户权限信息中间件
* @returns {Function} 中间件函数
*/
const getUserPermissions = async (req, res, next) => {
try {
if (!req.user || !req.user.id) {
return next();
}
// 获取用户信息(包含角色)
const user = await User.findByPk(req.user.id, {
include: [{
model: Role,
as: 'role',
attributes: ['id', 'name']
}]
});
if (user && user.role) {
req.currentUser = {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
permissions: getRolePermissions(user.role.name)
};
}
next();
} catch (error) {
console.error('获取用户权限信息错误:', error);
next();
}
};
module.exports = {
requirePermission,
requireRole,
requireAdmin,
requireFarmManager,
requireInspector,
getUserPermissions,
};

View File

@@ -0,0 +1,113 @@
/**
* 搜索请求日志中间件
* @description 记录所有搜索相关的请求和响应
*/
const { FormLog } = require('../models');
/**
* 搜索请求日志中间件
* @param {Object} req - 请求对象
* @param {Object} res - 响应对象
* @param {Function} next - 下一个中间件
*/
const searchLogger = async (req, res, next) => {
const startTime = Date.now();
const requestId = Math.random().toString(36).substr(2, 9);
// 为请求添加唯一ID
req.searchRequestId = requestId;
console.log(`🌐 [搜索中间件] 请求开始:`, {
requestId: requestId,
method: req.method,
url: req.originalUrl,
query: req.query,
body: req.body,
headers: {
'user-agent': req.get('User-Agent'),
'content-type': req.get('Content-Type'),
'accept': req.get('Accept'),
'referer': req.get('Referer')
},
ip: req.ip || req.connection.remoteAddress,
timestamp: new Date().toISOString()
});
// 记录请求日志到数据库
try {
await FormLog.create({
action: 'search_request',
module: 'farm_search',
userId: req.user ? req.user.id : null,
formData: JSON.stringify({
requestId: requestId,
method: req.method,
url: req.originalUrl,
query: req.query,
body: req.body,
userAgent: req.get('User-Agent'),
clientIP: req.ip || req.connection.remoteAddress,
timestamp: new Date().toISOString()
}),
oldValues: null,
newValues: JSON.stringify(req.query),
success: true,
errorMessage: null
});
} catch (logError) {
console.error('❌ [搜索中间件] 记录请求日志失败:', logError);
}
// 监听响应
const originalSend = res.send;
res.send = function(data) {
const endTime = Date.now();
const responseTime = endTime - startTime;
console.log(`📤 [搜索中间件] 响应完成:`, {
requestId: requestId,
statusCode: res.statusCode,
responseTime: responseTime + 'ms',
dataSize: data ? data.length : 0,
timestamp: new Date().toISOString()
});
// 记录响应日志到数据库
try {
let responseData;
try {
responseData = JSON.parse(data);
} catch (e) {
responseData = { raw: data };
}
FormLog.create({
action: 'search_response',
module: 'farm_search',
userId: req.user ? req.user.id : null,
formData: JSON.stringify({
requestId: requestId,
statusCode: res.statusCode,
responseTime: responseTime,
dataSize: data ? data.length : 0,
success: res.statusCode < 400,
timestamp: new Date().toISOString()
}),
oldValues: null,
newValues: JSON.stringify(responseData),
success: res.statusCode < 400,
errorMessage: res.statusCode >= 400 ? `HTTP ${res.statusCode}` : null
});
} catch (logError) {
console.error('❌ [搜索中间件] 记录响应日志失败:', logError);
}
// 调用原始send方法
originalSend.call(this, data);
};
next();
};
module.exports = searchLogger;

View File

@@ -0,0 +1,299 @@
/**
* 安全增强中间件
* @file security.js
* @description 实现登录失败限制、会话超时、输入验证等安全功能
*/
const rateLimit = require('express-rate-limit');
const logger = require('../utils/logger');
// 登录失败次数记录
const loginAttempts = new Map();
const MAX_LOGIN_ATTEMPTS = 5; // 最大登录尝试次数
const LOCKOUT_DURATION = 15 * 60 * 1000; // 锁定15分钟
/**
* 登录失败次数限制中间件
*/
const loginAttemptsLimiter = (req, res, next) => {
const clientIP = req.ip || req.connection.remoteAddress;
const identifier = req.body.username || clientIP; // 使用用户名或IP作为标识
const now = Date.now();
const attempts = loginAttempts.get(identifier) || { count: 0, firstAttempt: now, lockedUntil: 0 };
// 检查是否仍在锁定期内
if (attempts.lockedUntil > now) {
const remainingTime = Math.ceil((attempts.lockedUntil - now) / 1000 / 60); // 分钟
logger.warn(`用户 ${identifier} 尝试在锁定期内登录, IP: ${clientIP}`);
return res.status(429).json({
success: false,
message: `登录失败次数过多,请 ${remainingTime} 分钟后再试`,
lockedUntil: new Date(attempts.lockedUntil).toISOString()
});
}
// 重置过期的记录
if (now - attempts.firstAttempt > LOCKOUT_DURATION) {
attempts.count = 0;
attempts.firstAttempt = now;
}
// 检查失败次数
if (attempts.count >= MAX_LOGIN_ATTEMPTS) {
attempts.lockedUntil = now + LOCKOUT_DURATION;
loginAttempts.set(identifier, attempts);
logger.warn(`用户 ${identifier} 登录失败次数达到上限,已锁定, IP: ${clientIP}`);
return res.status(429).json({
success: false,
message: `登录失败次数过多已锁定15分钟`,
lockedUntil: new Date(attempts.lockedUntil).toISOString()
});
}
// 将attempts信息附加到请求对象供后续使用
req.loginAttempts = attempts;
req.loginIdentifier = identifier;
next();
};
/**
* 记录登录失败
*/
const recordLoginFailure = (req, res, next) => {
// 检查响应状态如果是401认证失败记录失败次数
const originalSend = res.send;
res.send = function(data) {
if (res.statusCode === 401 && req.loginIdentifier) {
const attempts = req.loginAttempts;
attempts.count++;
loginAttempts.set(req.loginIdentifier, attempts);
logger.warn(`用户 ${req.loginIdentifier} 登录失败,失败次数: ${attempts.count}/${MAX_LOGIN_ATTEMPTS}, IP: ${req.ip}`);
} else if (res.statusCode === 200 && req.loginIdentifier) {
// 登录成功,清除失败记录
loginAttempts.delete(req.loginIdentifier);
logger.info(`用户 ${req.loginIdentifier} 登录成功,已清除失败记录, IP: ${req.ip}`);
}
return originalSend.call(this, data);
};
next();
};
/**
* 清除过期的登录失败记录
*/
const cleanupExpiredAttempts = () => {
const now = Date.now();
const expiredKeys = [];
for (const [key, attempts] of loginAttempts.entries()) {
if (now - attempts.firstAttempt > LOCKOUT_DURATION * 2) { // 保留双倍锁定时间
expiredKeys.push(key);
}
}
expiredKeys.forEach(key => loginAttempts.delete(key));
if (expiredKeys.length > 0) {
logger.info(`清理了 ${expiredKeys.length} 个过期的登录失败记录`);
}
};
// 每小时清理一次过期记录
setInterval(cleanupExpiredAttempts, 60 * 60 * 1000);
/**
* API请求频率限制
*/
const apiRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟窗口
max: 1000, // 限制每个IP每15分钟最多1000个请求
message: {
success: false,
message: '请求过于频繁,请稍后再试'
},
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
logger.warn(`API请求频率超限, IP: ${req.ip}, URL: ${req.originalUrl}`);
res.status(429).json({
success: false,
message: '请求过于频繁,请稍后再试'
});
}
});
/**
* 登录请求频率限制
*/
const loginRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟窗口
max: 10, // 限制每个IP每15分钟最多10次登录尝试
message: {
success: false,
message: '登录请求过于频繁,请稍后再试'
},
skipSuccessfulRequests: true, // 成功请求不计入限制
handler: (req, res) => {
logger.warn(`登录请求频率超限, IP: ${req.ip}`);
res.status(429).json({
success: false,
message: '登录请求过于频繁请15分钟后再试'
});
}
});
/**
* 输入验证和XSS防护
*/
const inputSanitizer = (req, res, next) => {
// 递归清理对象中的危险字符
const sanitizeObject = (obj) => {
if (obj === null || obj === undefined) return obj;
if (typeof obj === 'string') {
// 移除潜在的XSS攻击字符
return obj
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') // 移除script标签
.replace(/javascript:/gi, '') // 移除javascript协议
.replace(/on\w+\s*=/gi, '') // 移除事件处理器
.trim();
}
if (Array.isArray(obj)) {
return obj.map(sanitizeObject);
}
if (typeof obj === 'object') {
const sanitized = {};
for (const [key, value] of Object.entries(obj)) {
sanitized[key] = sanitizeObject(value);
}
return sanitized;
}
return obj;
};
// 清理请求体
if (req.body) {
req.body = sanitizeObject(req.body);
}
// 清理查询参数
if (req.query) {
req.query = sanitizeObject(req.query);
}
next();
};
/**
* 会话超时检查
*/
const sessionTimeoutCheck = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token) {
try {
const jwt = require('jsonwebtoken');
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your_jwt_secret_key');
// 检查token是否即将过期剩余时间少于1小时
const now = Math.floor(Date.now() / 1000);
const timeUntilExpiry = decoded.exp - now;
if (timeUntilExpiry < 3600) { // 1小时 = 3600秒
res.set('X-Token-Expiry-Warning', 'true');
res.set('X-Token-Expires-In', timeUntilExpiry.toString());
}
} catch (error) {
// Token无效或已过期不做特殊处理让后续中间件处理
}
}
next();
};
/**
* 安全响应头设置
*/
const securityHeaders = (req, res, next) => {
// 防止点击劫持
res.set('X-Frame-Options', 'DENY');
// 防止MIME类型嗅探
res.set('X-Content-Type-Options', 'nosniff');
// XSS保护
res.set('X-XSS-Protection', '1; mode=block');
// 引用者策略
res.set('Referrer-Policy', 'strict-origin-when-cross-origin');
// 内容安全策略 - 允许百度地图API
const cspPolicy = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' api.map.baidu.com apimaponline0.bdimg.com apimaponline1.bdimg.com apimaponline2.bdimg.com apimaponline3.bdimg.com dlswbr.baidu.com miao.baidu.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: api.map.baidu.com apimaponline0.bdimg.com apimaponline1.bdimg.com apimaponline2.bdimg.com apimaponline3.bdimg.com dlswbr.baidu.com miao.baidu.com",
"connect-src 'self' api.map.baidu.com apimaponline0.bdimg.com apimaponline1.bdimg.com apimaponline2.bdimg.com apimaponline3.bdimg.com dlswbr.baidu.com miao.baidu.com",
"frame-src 'self'"
].join('; ');
res.set('Content-Security-Policy', cspPolicy);
next();
};
/**
* 获取登录失败统计信息
* @returns {Object} 统计信息
*/
const getLoginAttemptStats = () => {
const now = Date.now();
let totalAttempts = 0;
let lockedAccounts = 0;
let recentFailures = 0;
for (const [identifier, attempts] of loginAttempts.entries()) {
totalAttempts += attempts.count;
if (attempts.lockedUntil > now) {
lockedAccounts++;
}
if (now - attempts.firstAttempt < 60 * 60 * 1000) { // 最近1小时
recentFailures += attempts.count;
}
}
return {
totalTrackedIdentifiers: loginAttempts.size,
totalFailedAttempts: totalAttempts,
currentlyLockedAccounts: lockedAccounts,
recentHourFailures: recentFailures,
maxAttemptsAllowed: MAX_LOGIN_ATTEMPTS,
lockoutDurationMinutes: LOCKOUT_DURATION / 60 / 1000
};
};
module.exports = {
loginAttemptsLimiter,
recordLoginFailure,
apiRateLimiter,
loginRateLimiter,
inputSanitizer,
sessionTimeoutCheck,
securityHeaders,
getLoginAttemptStats,
cleanupExpiredAttempts
};