/** * 安全中间件 * @file security.js * @description 处理安全相关的中间件 */ const rateLimit = require('express-rate-limit'); const helmet = require('helmet'); const { body, validationResult } = require('express-validator'); /** * API请求频率限制 */ const apiRateLimiter = rateLimit({ windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15分钟 max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100, // 限制每个IP 15分钟内最多100个请求 message: { success: false, message: '请求过于频繁,请稍后再试' }, standardHeaders: true, legacyHeaders: false, handler: (req, res) => { res.status(429).json({ success: false, message: '请求过于频繁,请稍后再试' }); } }); /** * 登录请求频率限制(更严格) */ const loginRateLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15分钟 max: 5, // 限制每个IP 15分钟内最多5次登录尝试 message: { success: false, message: '登录尝试次数过多,请15分钟后再试' }, skipSuccessfulRequests: true, handler: (req, res) => { res.status(429).json({ success: false, message: '登录尝试次数过多,请15分钟后再试' }); } }); /** * 安全头部设置 */ const securityHeaders = helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'"], fontSrc: ["'self'"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"] } }, crossOriginEmbedderPolicy: false }); /** * 输入数据清理 */ const inputSanitizer = (req, res, next) => { // 清理请求体中的危险字符 const sanitizeObject = (obj) => { if (typeof obj !== 'object' || obj === null) return obj; for (const key in obj) { if (typeof obj[key] === 'string') { // 移除潜在的XSS攻击字符 obj[key] = obj[key] .replace(/)<[^<]*)*<\/script>/gi, '') .replace(/javascript:/gi, '') .replace(/on\w+\s*=/gi, ''); } else if (typeof obj[key] === 'object') { sanitizeObject(obj[key]); } } }; if (req.body) sanitizeObject(req.body); if (req.query) sanitizeObject(req.query); if (req.params) sanitizeObject(req.params); next(); }; /** * 会话超时检查 */ const sessionTimeoutCheck = (req, res, next) => { if (req.user && req.user.last_login) { const lastLogin = new Date(req.user.last_login); const now = new Date(); const timeout = 24 * 60 * 60 * 1000; // 24小时 if (now - lastLogin > timeout) { return res.status(401).json({ success: false, message: '会话已超时,请重新登录' }); } } next(); }; /** * 验证错误处理中间件 */ const handleValidationErrors = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, message: '输入数据验证失败', errors: errors.array() }); } next(); }; /** * 银行账户验证规则 */ const validateAccountNumber = [ body('account_number') .isLength({ min: 16, max: 20 }) .withMessage('账户号码长度必须在16-20位之间') .matches(/^\d+$/) .withMessage('账户号码只能包含数字'), handleValidationErrors ]; /** * 金额验证规则 */ const validateAmount = [ body('amount') .isFloat({ min: 0.01 }) .withMessage('金额必须大于0') .custom((value) => { // 检查金额精度(最多2位小数) if (value.toString().split('.')[1] && value.toString().split('.')[1].length > 2) { throw new Error('金额最多支持2位小数'); } return true; }), handleValidationErrors ]; /** * 身份证号验证规则 */ const validateIdCard = [ body('id_card') .matches(/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/) .withMessage('身份证号码格式不正确'), handleValidationErrors ]; /** * 手机号验证规则 */ const validatePhone = [ body('phone') .matches(/^1[3-9]\d{9}$/) .withMessage('手机号码格式不正确'), handleValidationErrors ]; /** * 密码验证规则 */ const validatePassword = [ body('password') .isLength({ min: 6, max: 20 }) .withMessage('密码长度必须在6-20位之间') .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/) .withMessage('密码必须包含大小写字母和数字'), handleValidationErrors ]; /** * 防止SQL注入的查询参数验证 */ const validateQueryParams = (req, res, next) => { const dangerousPatterns = [ /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|UNION|SCRIPT)\b)/i, /(--|\/\*|\*\/|xp_|sp_)/i, /(\bOR\b|\bAND\b).*(\bOR\b|\bAND\b)/i ]; const checkObject = (obj) => { for (const key in obj) { if (typeof obj[key] === 'string') { for (const pattern of dangerousPatterns) { if (pattern.test(obj[key])) { return res.status(400).json({ success: false, message: '检测到潜在的安全威胁' }); } } } else if (typeof obj[key] === 'object' && obj[key] !== null) { checkObject(obj[key]); } } }; checkObject(req.query); checkObject(req.body); checkObject(req.params); next(); }; module.exports = { apiRateLimiter, loginRateLimiter, securityHeaders, inputSanitizer, sessionTimeoutCheck, handleValidationErrors, validateAccountNumber, validateAmount, validateIdCard, validatePhone, validatePassword, validateQueryParams };