Files
nxxmdata/bank-backend/middleware/security.js
2025-09-17 18:04:28 +08:00

239 lines
5.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 安全中间件
* @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\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/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
};