修改管理后台
This commit is contained in:
@@ -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));
|
||||
|
||||
316
backend/middleware/autoOperationLogger.js
Normal file
316
backend/middleware/autoOperationLogger.js
Normal 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
|
||||
};
|
||||
66
backend/middleware/operationLogAuth.js
Normal file
66
backend/middleware/operationLogAuth.js
Normal 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
|
||||
};
|
||||
216
backend/middleware/operationLogger.js
Normal file
216
backend/middleware/operationLogger.js
Normal 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
|
||||
};
|
||||
224
backend/middleware/permission.js
Normal file
224
backend/middleware/permission.js
Normal 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,
|
||||
};
|
||||
113
backend/middleware/search-logger.js
Normal file
113
backend/middleware/search-logger.js
Normal 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;
|
||||
299
backend/middleware/security.js
Normal file
299
backend/middleware/security.js
Normal 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
|
||||
};
|
||||
Reference in New Issue
Block a user