更新政府端和银行端

This commit is contained in:
2025-09-17 18:04:28 +08:00
parent f35ceef31f
commit e4287b83fe
185 changed files with 78320 additions and 189 deletions

View File

@@ -0,0 +1,226 @@
/**
* 认证中间件
* @file auth.js
* @description 处理用户认证和授权
*/
const jwt = require('jsonwebtoken');
const { User, Role } = require('../models');
/**
* 验证JWT令牌
* @param {Object} req 请求对象
* @param {Object} res 响应对象
* @param {Function} next 下一个中间件
*/
const verifyToken = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
success: false,
message: '访问被拒绝,未提供令牌'
});
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByPk(decoded.id, {
include: [{
model: Role,
as: 'role'
}]
});
if (!user) {
return res.status(401).json({
success: false,
message: '令牌无效,用户不存在'
});
}
if (user.status !== 'active') {
return res.status(401).json({
success: false,
message: '账户已被禁用'
});
}
req.user = user;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: '令牌已过期'
});
} else if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: '令牌无效'
});
} else {
console.error('认证中间件错误:', error);
return res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
}
};
/**
* 检查用户角色权限
* @param {String|Array} roles 允许的角色
* @returns {Function} 中间件函数
*/
const requireRole = (roles) => {
return async (req, res, next) => {
try {
if (!req.user) {
return res.status(401).json({
success: false,
message: '请先登录'
});
}
const userRole = req.user.role;
if (!userRole) {
return res.status(403).json({
success: false,
message: '用户角色未分配'
});
}
const allowedRoles = Array.isArray(roles) ? roles : [roles];
if (!allowedRoles.includes(userRole.name)) {
return res.status(403).json({
success: false,
message: '权限不足'
});
}
next();
} catch (error) {
console.error('角色权限检查错误:', error);
return res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
};
};
/**
* 检查用户权限级别
* @param {Number} minLevel 最小权限级别
* @returns {Function} 中间件函数
*/
const requireLevel = (minLevel) => {
return async (req, res, next) => {
try {
if (!req.user) {
return res.status(401).json({
success: false,
message: '请先登录'
});
}
const userRole = req.user.role;
if (!userRole || userRole.level < minLevel) {
return res.status(403).json({
success: false,
message: '权限级别不足'
});
}
next();
} catch (error) {
console.error('权限级别检查错误:', error);
return res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
};
};
/**
* 可选认证中间件(不强制要求登录)
* @param {Object} req 请求对象
* @param {Object} res 响应对象
* @param {Function} next 下一个中间件
*/
const optionalAuth = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (token) {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByPk(decoded.id, {
include: [{
model: Role,
as: 'role'
}]
});
if (user && user.status === 'active') {
req.user = user;
}
}
next();
} catch (error) {
// 可选认证失败时不返回错误,继续执行
next();
}
};
/**
* 检查账户所有权
* @param {Object} req 请求对象
* @param {Object} res 响应对象
* @param {Function} next 下一个中间件
*/
const checkAccountOwnership = async (req, res, next) => {
try {
const { accountId } = req.params;
const userId = req.user.id;
// 管理员可以访问所有账户
if (req.user.role && req.user.role.name === 'admin') {
return next();
}
const { Account } = require('../models');
const account = await Account.findOne({
where: {
id: accountId,
user_id: userId
}
});
if (!account) {
return res.status(403).json({
success: false,
message: '无权访问该账户'
});
}
req.account = account;
next();
} catch (error) {
console.error('账户所有权检查错误:', error);
return res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
};
module.exports = {
verifyToken,
requireRole,
requireLevel,
optionalAuth,
checkAccountOwnership
};

View File

@@ -0,0 +1,239 @@
/**
* 安全中间件
* @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
};