# 解班客安全和权限管理文档 ## 📋 概述 本文档详细描述解班客项目的安全架构、权限管理体系、安全防护措施和安全最佳实践。通过多层次的安全防护,确保系统和用户数据的安全性。 ## 🎯 安全目标 ### 核心安全原则 - **最小权限原则**: 用户和系统组件仅获得完成任务所需的最小权限 - **深度防御**: 多层安全防护,避免单点失效 - **零信任架构**: 不信任任何内部或外部实体,持续验证 - **数据保护**: 全生命周期数据安全保护 - **合规性**: 符合相关法律法规和行业标准 ### 安全目标 - **身份认证**: 确保用户身份的真实性和唯一性 - **访问控制**: 基于角色和权限的精细化访问控制 - **数据安全**: 敏感数据加密存储和传输 - **系统安全**: 防范各类网络攻击和安全威胁 - **审计追踪**: 完整的操作日志和安全审计 ## 🏗️ 安全架构 ### 整体安全架构 ```mermaid graph TB subgraph "外部防护层" A[CDN/WAF] --> B[负载均衡器] B --> C[反向代理] end subgraph "应用层安全" C --> D[API网关] D --> E[身份认证] E --> F[权限控制] F --> G[业务逻辑] end subgraph "数据层安全" G --> H[数据加密] H --> I[数据库] I --> J[备份系统] end subgraph "监控层" K[安全监控] --> L[日志分析] L --> M[告警系统] M --> N[事件响应] end style A fill:#ff9999 style E fill:#99ccff style H fill:#99ff99 style K fill:#ffcc99 ``` ### 安全分层 #### 1. 网络安全层 - **防火墙配置**: 端口访问控制和流量过滤 - **DDoS防护**: 分布式拒绝服务攻击防护 - **SSL/TLS加密**: 数据传输加密 - **VPN访问**: 管理员远程安全访问 #### 2. 应用安全层 - **身份认证**: JWT令牌和多因素认证 - **权限控制**: RBAC基于角色的访问控制 - **输入验证**: 防止注入攻击 - **会话管理**: 安全的会话处理 #### 3. 数据安全层 - **数据加密**: 敏感数据加密存储 - **数据脱敏**: 测试环境数据脱敏 - **备份安全**: 加密备份和异地存储 - **数据销毁**: 安全的数据删除 ## 🔐 身份认证系统 ### JWT认证机制 #### Token结构 ```javascript // JWT Token结构 { "header": { "alg": "HS256", "typ": "JWT" }, "payload": { "user_id": 12345, "username": "user@example.com", "role": "user", "permissions": ["read:animals", "create:adoption"], "iat": 1640995200, "exp": 1641081600, "jti": "unique-token-id" }, "signature": "encrypted-signature" } ``` #### 认证流程 ```javascript // 用户登录认证 async function authenticateUser(credentials) { try { // 1. 验证用户凭据 const user = await validateCredentials(credentials) if (!user) { throw new Error('用户名或密码错误') } // 2. 检查账户状态 if (user.status !== 'active') { throw new Error('账户已被禁用') } // 3. 记录登录日志 await logSecurityEvent({ type: 'LOGIN_SUCCESS', user_id: user.id, ip_address: credentials.ip, user_agent: credentials.userAgent, timestamp: new Date() }) // 4. 生成访问令牌 const accessToken = generateAccessToken(user) const refreshToken = generateRefreshToken(user) // 5. 存储刷新令牌 await storeRefreshToken(user.id, refreshToken) return { access_token: accessToken, refresh_token: refreshToken, expires_in: 3600, user: { id: user.id, username: user.username, role: user.role, permissions: user.permissions } } } catch (error) { // 记录失败日志 await logSecurityEvent({ type: 'LOGIN_FAILED', username: credentials.username, ip_address: credentials.ip, error: error.message, timestamp: new Date() }) throw error } } // 生成访问令牌 function generateAccessToken(user) { const payload = { user_id: user.id, username: user.username, role: user.role, permissions: user.permissions, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 3600, // 1小时过期 jti: generateUniqueId() } return jwt.sign(payload, process.env.JWT_SECRET, { algorithm: 'HS256' }) } // 令牌验证中间件 function verifyToken(req, res, next) { try { const token = extractTokenFromHeader(req.headers.authorization) if (!token) { return res.status(401).json({ error: '缺少访问令牌' }) } // 验证令牌 const decoded = jwt.verify(token, process.env.JWT_SECRET) // 检查令牌是否在黑名单中 if (await isTokenBlacklisted(decoded.jti)) { return res.status(401).json({ error: '令牌已失效' }) } // 将用户信息添加到请求对象 req.user = { id: decoded.user_id, username: decoded.username, role: decoded.role, permissions: decoded.permissions } next() } catch (error) { if (error.name === 'TokenExpiredError') { return res.status(401).json({ error: '令牌已过期' }) } else if (error.name === 'JsonWebTokenError') { return res.status(401).json({ error: '无效的令牌' }) } return res.status(500).json({ error: '令牌验证失败' }) } } ``` ### 多因素认证 (MFA) #### 短信验证码 ```javascript // 发送短信验证码 async function sendSMSCode(phoneNumber, purpose) { try { // 1. 生成6位数字验证码 const code = Math.floor(100000 + Math.random() * 900000).toString() // 2. 设置过期时间(5分钟) const expiresAt = new Date(Date.now() + 5 * 60 * 1000) // 3. 存储验证码 await redis.setex( `sms_code:${phoneNumber}:${purpose}`, 300, // 5分钟过期 JSON.stringify({ code: await bcrypt.hash(code, 10), // 加密存储 attempts: 0, created_at: new Date() }) ) // 4. 发送短信 await smsService.send({ to: phoneNumber, message: `【解班客】您的验证码是:${code},5分钟内有效,请勿泄露。` }) // 5. 记录发送日志 await logSecurityEvent({ type: 'SMS_CODE_SENT', phone_number: phoneNumber, purpose: purpose, timestamp: new Date() }) return { success: true, message: '验证码已发送' } } catch (error) { logger.error('发送短信验证码失败:', error) throw new Error('发送验证码失败') } } // 验证短信验证码 async function verifySMSCode(phoneNumber, code, purpose) { try { const key = `sms_code:${phoneNumber}:${purpose}` const storedData = await redis.get(key) if (!storedData) { throw new Error('验证码已过期或不存在') } const { code: hashedCode, attempts } = JSON.parse(storedData) // 检查尝试次数 if (attempts >= 3) { await redis.del(key) throw new Error('验证码尝试次数过多,请重新获取') } // 验证验证码 const isValid = await bcrypt.compare(code, hashedCode) if (!isValid) { // 增加尝试次数 await redis.setex( key, await redis.ttl(key), JSON.stringify({ code: hashedCode, attempts: attempts + 1, created_at: new Date() }) ) throw new Error('验证码错误') } // 验证成功,删除验证码 await redis.del(key) // 记录验证日志 await logSecurityEvent({ type: 'SMS_CODE_VERIFIED', phone_number: phoneNumber, purpose: purpose, timestamp: new Date() }) return { success: true, message: '验证码验证成功' } } catch (error) { logger.error('验证短信验证码失败:', error) throw error } } ``` #### TOTP认证器 ```javascript // TOTP (Time-based One-Time Password) 实现 const speakeasy = require('speakeasy') const QRCode = require('qrcode') // 生成TOTP密钥 async function generateTOTPSecret(userId) { const secret = speakeasy.generateSecret({ name: `解班客 (${userId})`, issuer: '解班客', length: 32 }) // 存储密钥到数据库 await UserSecurity.create({ user_id: userId, totp_secret: encrypt(secret.base32), totp_enabled: false, created_at: new Date() }) // 生成二维码 const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url) return { secret: secret.base32, qr_code: qrCodeUrl, manual_entry_key: secret.base32 } } // 验证TOTP令牌 async function verifyTOTPToken(userId, token) { try { const userSecurity = await UserSecurity.findOne({ where: { user_id: userId, totp_enabled: true } }) if (!userSecurity) { throw new Error('TOTP未启用') } const secret = decrypt(userSecurity.totp_secret) const verified = speakeasy.totp.verify({ secret: secret, encoding: 'base32', token: token, window: 2 // 允许时间窗口偏差 }) if (!verified) { // 记录失败尝试 await logSecurityEvent({ type: 'TOTP_VERIFICATION_FAILED', user_id: userId, timestamp: new Date() }) throw new Error('TOTP令牌无效') } // 记录成功验证 await logSecurityEvent({ type: 'TOTP_VERIFICATION_SUCCESS', user_id: userId, timestamp: new Date() }) return { success: true } } catch (error) { logger.error('TOTP验证失败:', error) throw error } } ``` ## 👥 权限管理系统 ### RBAC权限模型 #### 权限数据模型 ```sql -- 角色表 CREATE TABLE roles ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE, display_name VARCHAR(100) NOT NULL, description TEXT, is_system BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 权限表 CREATE TABLE permissions ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL UNIQUE, display_name VARCHAR(100) NOT NULL, description TEXT, resource VARCHAR(50) NOT NULL, action VARCHAR(50) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 角色权限关联表 CREATE TABLE role_permissions ( id INT PRIMARY KEY AUTO_INCREMENT, role_id INT NOT NULL, permission_id INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE, UNIQUE KEY unique_role_permission (role_id, permission_id) ); -- 用户角色关联表 CREATE TABLE user_roles ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, role_id INT NOT NULL, assigned_by INT, assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, FOREIGN KEY (assigned_by) REFERENCES users(id), UNIQUE KEY unique_user_role (user_id, role_id) ); -- 用户直接权限表(特殊权限) CREATE TABLE user_permissions ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, permission_id INT NOT NULL, granted_by INT, granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE, FOREIGN KEY (granted_by) REFERENCES users(id), UNIQUE KEY unique_user_permission (user_id, permission_id) ); ``` #### 权限检查中间件 ```javascript // 权限检查中间件 function requirePermission(resource, action) { return async (req, res, next) => { try { const userId = req.user.id const hasPermission = await checkUserPermission(userId, resource, action) if (!hasPermission) { // 记录权限拒绝日志 await logSecurityEvent({ type: 'PERMISSION_DENIED', user_id: userId, resource: resource, action: action, ip_address: req.ip, user_agent: req.get('User-Agent'), timestamp: new Date() }) return res.status(403).json({ error: '权限不足', message: `您没有执行 ${action} 操作 ${resource} 的权限` }) } next() } catch (error) { logger.error('权限检查失败:', error) return res.status(500).json({ error: '权限检查失败' }) } } } // 检查用户权限 async function checkUserPermission(userId, resource, action) { try { // 1. 检查用户直接权限 const directPermission = await UserPermission.findOne({ include: [{ model: Permission, where: { resource, action } }], where: { user_id: userId, [Op.or]: [ { expires_at: null }, { expires_at: { [Op.gt]: new Date() } } ] } }) if (directPermission) { return true } // 2. 检查角色权限 const rolePermissions = await UserRole.findAll({ include: [{ model: Role, include: [{ model: Permission, where: { resource, action }, through: { attributes: [] } }] }], where: { user_id: userId, [Op.or]: [ { expires_at: null }, { expires_at: { [Op.gt]: new Date() } } ] } }) return rolePermissions.length > 0 } catch (error) { logger.error('权限检查错误:', error) return false } } // 获取用户所有权限 async function getUserPermissions(userId) { try { const permissions = new Set() // 1. 获取直接权限 const directPermissions = await UserPermission.findAll({ include: [Permission], where: { user_id: userId, [Op.or]: [ { expires_at: null }, { expires_at: { [Op.gt]: new Date() } } ] } }) directPermissions.forEach(up => { permissions.add(`${up.Permission.resource}:${up.Permission.action}`) }) // 2. 获取角色权限 const rolePermissions = await UserRole.findAll({ include: [{ model: Role, include: [{ model: Permission, through: { attributes: [] } }] }], where: { user_id: userId, [Op.or]: [ { expires_at: null }, { expires_at: { [Op.gt]: new Date() } } ] } }) rolePermissions.forEach(ur => { ur.Role.Permissions.forEach(permission => { permissions.add(`${permission.resource}:${permission.action}`) }) }) return Array.from(permissions) } catch (error) { logger.error('获取用户权限失败:', error) return [] } } ``` ### 角色管理 #### 预定义角色 ```javascript // 系统预定义角色 const SYSTEM_ROLES = { SUPER_ADMIN: { name: 'super_admin', display_name: '超级管理员', description: '拥有系统所有权限', permissions: ['*:*'] // 通配符表示所有权限 }, ADMIN: { name: 'admin', display_name: '管理员', description: '系统管理员,可管理用户和内容', permissions: [ 'users:read', 'users:create', 'users:update', 'users:delete', 'animals:read', 'animals:create', 'animals:update', 'animals:delete', 'adoptions:read', 'adoptions:update', 'adoptions:approve', 'content:read', 'content:create', 'content:update', 'content:delete', 'reports:read', 'system:monitor' ] }, MODERATOR: { name: 'moderator', display_name: '内容审核员', description: '负责内容审核和动物信息管理', permissions: [ 'animals:read', 'animals:create', 'animals:update', 'adoptions:read', 'adoptions:update', 'content:read', 'content:update', 'reports:read' ] }, USER: { name: 'user', display_name: '普通用户', description: '普通用户,可浏览和申请认领', permissions: [ 'animals:read', 'adoptions:create', 'adoptions:read_own', 'profile:read', 'profile:update' ] }, VOLUNTEER: { name: 'volunteer', display_name: '志愿者', description: '志愿者,可协助动物信息维护', permissions: [ 'animals:read', 'animals:update', 'adoptions:read', 'content:read', 'profile:read', 'profile:update' ] } } // 初始化系统角色 async function initializeSystemRoles() { try { for (const [key, roleData] of Object.entries(SYSTEM_ROLES)) { // 创建或更新角色 const [role] = await Role.findOrCreate({ where: { name: roleData.name }, defaults: { display_name: roleData.display_name, description: roleData.description, is_system: true } }) // 处理权限 if (roleData.permissions.includes('*:*')) { // 超级管理员拥有所有权限 const allPermissions = await Permission.findAll() await role.setPermissions(allPermissions) } else { // 设置指定权限 const permissions = await Permission.findAll({ where: { name: { [Op.in]: roleData.permissions } } }) await role.setPermissions(permissions) } } logger.info('系统角色初始化完成') } catch (error) { logger.error('系统角色初始化失败:', error) throw error } } ``` #### 动态权限管理 ```javascript // 权限管理服务 class PermissionService { // 创建权限 static async createPermission(permissionData) { try { const permission = await Permission.create({ name: `${permissionData.resource}:${permissionData.action}`, display_name: permissionData.display_name, description: permissionData.description, resource: permissionData.resource, action: permissionData.action }) logger.info(`权限创建成功: ${permission.name}`) return permission } catch (error) { logger.error('权限创建失败:', error) throw error } } // 分配角色给用户 static async assignRoleToUser(userId, roleId, assignedBy, expiresAt = null) { try { const userRole = await UserRole.create({ user_id: userId, role_id: roleId, assigned_by: assignedBy, expires_at: expiresAt }) // 记录权限变更日志 await logSecurityEvent({ type: 'ROLE_ASSIGNED', user_id: userId, role_id: roleId, assigned_by: assignedBy, expires_at: expiresAt, timestamp: new Date() }) // 清除用户权限缓存 await this.clearUserPermissionCache(userId) return userRole } catch (error) { logger.error('角色分配失败:', error) throw error } } // 撤销用户角色 static async revokeRoleFromUser(userId, roleId, revokedBy) { try { const result = await UserRole.destroy({ where: { user_id: userId, role_id: roleId } }) if (result > 0) { // 记录权限变更日志 await logSecurityEvent({ type: 'ROLE_REVOKED', user_id: userId, role_id: roleId, revoked_by: revokedBy, timestamp: new Date() }) // 清除用户权限缓存 await this.clearUserPermissionCache(userId) } return result > 0 } catch (error) { logger.error('角色撤销失败:', error) throw error } } // 授予用户直接权限 static async grantPermissionToUser(userId, permissionId, grantedBy, expiresAt = null) { try { const userPermission = await UserPermission.create({ user_id: userId, permission_id: permissionId, granted_by: grantedBy, expires_at: expiresAt }) // 记录权限变更日志 await logSecurityEvent({ type: 'PERMISSION_GRANTED', user_id: userId, permission_id: permissionId, granted_by: grantedBy, expires_at: expiresAt, timestamp: new Date() }) // 清除用户权限缓存 await this.clearUserPermissionCache(userId) return userPermission } catch (error) { logger.error('权限授予失败:', error) throw error } } // 清除用户权限缓存 static async clearUserPermissionCache(userId) { try { await redis.del(`user_permissions:${userId}`) logger.info(`用户权限缓存已清除: ${userId}`) } catch (error) { logger.error('清除权限缓存失败:', error) } } // 获取用户权限(带缓存) static async getUserPermissionsWithCache(userId) { try { const cacheKey = `user_permissions:${userId}` let permissions = await redis.get(cacheKey) if (permissions) { return JSON.parse(permissions) } permissions = await getUserPermissions(userId) // 缓存权限信息(5分钟) await redis.setex(cacheKey, 300, JSON.stringify(permissions)) return permissions } catch (error) { logger.error('获取用户权限失败:', error) return [] } } } ``` ## 🛡️ 安全防护措施 ### 输入验证和过滤 #### SQL注入防护 ```javascript // 使用参数化查询防止SQL注入 const { QueryTypes } = require('sequelize') // 错误示例 - 容易受到SQL注入攻击 async function searchAnimalsUnsafe(keyword) { const query = `SELECT * FROM animals WHERE name LIKE '%${keyword}%'` return await sequelize.query(query, { type: QueryTypes.SELECT }) } // 正确示例 - 使用参数化查询 async function searchAnimalsSafe(keyword) { const query = ` SELECT * FROM animals WHERE name LIKE :keyword OR description LIKE :keyword ` return await sequelize.query(query, { replacements: { keyword: `%${keyword}%` }, type: QueryTypes.SELECT }) } // 使用ORM的安全查询 async function searchAnimalsORM(keyword) { return await Animal.findAll({ where: { [Op.or]: [ { name: { [Op.like]: `%${keyword}%` } }, { description: { [Op.like]: `%${keyword}%` } } ] } }) } ``` #### XSS防护 ```javascript const DOMPurify = require('isomorphic-dompurify') const validator = require('validator') // XSS过滤中间件 function xssProtection(req, res, next) { // 递归清理对象中的所有字符串 function sanitizeObject(obj) { if (typeof obj === 'string') { return DOMPurify.sanitize(obj) } else if (Array.isArray(obj)) { return obj.map(sanitizeObject) } else if (obj && 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() } // 输入验证函数 function validateInput(data, rules) { const errors = [] for (const [field, rule] of Object.entries(rules)) { const value = data[field] // 必填验证 if (rule.required && (!value || value.trim() === '')) { errors.push(`${field} 是必填字段`) continue } if (value) { // 长度验证 if (rule.minLength && value.length < rule.minLength) { errors.push(`${field} 长度不能少于 ${rule.minLength} 个字符`) } if (rule.maxLength && value.length > rule.maxLength) { errors.push(`${field} 长度不能超过 ${rule.maxLength} 个字符`) } // 格式验证 if (rule.type === 'email' && !validator.isEmail(value)) { errors.push(`${field} 格式不正确`) } if (rule.type === 'phone' && !validator.isMobilePhone(value, 'zh-CN')) { errors.push(`${field} 手机号格式不正确`) } if (rule.type === 'url' && !validator.isURL(value)) { errors.push(`${field} URL格式不正确`) } // 自定义正则验证 if (rule.pattern && !rule.pattern.test(value)) { errors.push(`${field} 格式不符合要求`) } // 危险字符检测 if (containsDangerousChars(value)) { errors.push(`${field} 包含非法字符`) } } } return errors } // 检测危险字符 function containsDangerousChars(input) { const dangerousPatterns = [ /)<[^<]*)*<\/script>/gi, /javascript:/gi, /on\w+\s*=/gi, /eval\s*\(/gi, /expression\s*\(/gi ] return dangerousPatterns.some(pattern => pattern.test(input)) } ``` #### CSRF防护 ```javascript const csrf = require('csurf') const cookieParser = require('cookie-parser') // CSRF保护中间件配置 const csrfProtection = csrf({ cookie: { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict' } }) // 为前端提供CSRF令牌 app.get('/api/csrf-token', csrfProtection, (req, res) => { res.json({ csrfToken: req.csrfToken() }) }) // 应用CSRF保护到需要的路由 app.use('/api/admin', csrfProtection) app.use('/api/user/profile', csrfProtection) // 自定义CSRF错误处理 app.use((err, req, res, next) => { if (err.code === 'EBADCSRFTOKEN') { return res.status(403).json({ error: 'CSRF令牌无效', message: '请刷新页面后重试' }) } next(err) }) ``` ### 速率限制 #### API速率限制 ```javascript const rateLimit = require('express-rate-limit') const RedisStore = require('rate-limit-redis') const redis = require('redis') // Redis客户端 const redisClient = redis.createClient({ host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }) // 通用速率限制 const generalLimiter = rateLimit({ store: new RedisStore({ client: redisClient, prefix: 'rl:general:' }), windowMs: 15 * 60 * 1000, // 15分钟 max: 100, // 每个IP最多100个请求 message: { error: '请求过于频繁', message: '请稍后再试' }, standardHeaders: true, legacyHeaders: false }) // 登录速率限制 const loginLimiter = rateLimit({ store: new RedisStore({ client: redisClient, prefix: 'rl:login:' }), windowMs: 15 * 60 * 1000, // 15分钟 max: 5, // 每个IP最多5次登录尝试 skipSuccessfulRequests: true, message: { error: '登录尝试过于频繁', message: '请15分钟后再试' } }) // 注册速率限制 const registerLimiter = rateLimit({ store: new RedisStore({ client: redisClient, prefix: 'rl:register:' }), windowMs: 60 * 60 * 1000, // 1小时 max: 3, // 每个IP每小时最多3次注册 message: { error: '注册过于频繁', message: '请1小时后再试' } }) // 短信验证码速率限制 const smsLimiter = rateLimit({ store: new RedisStore({ client: redisClient, prefix: 'rl:sms:' }), windowMs: 60 * 1000, // 1分钟 max: 1, // 每分钟最多1条短信 keyGenerator: (req) => { return req.body.phone_number || req.ip }, message: { error: '短信发送过于频繁', message: '请1分钟后再试' } }) // 应用速率限制 app.use('/api', generalLimiter) app.use('/api/auth/login', loginLimiter) app.use('/api/auth/register', registerLimiter) app.use('/api/auth/send-sms', smsLimiter) // 自定义速率限制器 class CustomRateLimiter { constructor(options) { this.windowMs = options.windowMs this.max = options.max this.keyGenerator = options.keyGenerator || ((req) => req.ip) this.store = options.store || new Map() } middleware() { return async (req, res, next) => { try { const key = this.keyGenerator(req) const now = Date.now() const windowStart = now - this.windowMs // 获取当前窗口内的请求记录 const requests = await this.getRequests(key, windowStart) if (requests.length >= this.max) { return res.status(429).json({ error: '请求过于频繁', retryAfter: Math.ceil((requests[0].timestamp + this.windowMs - now) / 1000) }) } // 记录当前请求 await this.recordRequest(key, now) next() } catch (error) { logger.error('速率限制检查失败:', error) next() } } } async getRequests(key, windowStart) { // 从Redis获取请求记录 const data = await redis.get(`rate_limit:${key}`) if (!data) return [] const requests = JSON.parse(data) return requests.filter(req => req.timestamp > windowStart) } async recordRequest(key, timestamp) { const requests = await this.getRequests(key, 0) requests.push({ timestamp }) // 只保留窗口内的请求 const windowStart = timestamp - this.windowMs const validRequests = requests.filter(req => req.timestamp > windowStart) await redis.setex( `rate_limit:${key}`, Math.ceil(this.windowMs / 1000), JSON.stringify(validRequests) ) } } ``` ### 数据加密 #### 敏感数据加密 ```javascript const crypto = require('crypto') const bcrypt = require('bcrypt') // 加密配置 const ENCRYPTION_CONFIG = { algorithm: 'aes-256-gcm', keyLength: 32, ivLength: 16, tagLength: 16, saltRounds: 12 } // 生成加密密钥 function generateEncryptionKey() { return crypto.randomBytes(ENCRYPTION_CONFIG.keyLength) } // 对称加密 function encrypt(text, key = process.env.ENCRYPTION_KEY) { try { const iv = crypto.randomBytes(ENCRYPTION_CONFIG.ivLength) const cipher = crypto.createCipher(ENCRYPTION_CONFIG.algorithm, key) cipher.setAAD(Buffer.from('additional-data')) let encrypted = cipher.update(text, 'utf8', 'hex') encrypted += cipher.final('hex') const tag = cipher.getAuthTag() return { encrypted, iv: iv.toString('hex'), tag: tag.toString('hex') } } catch (error) { logger.error('加密失败:', error) throw new Error('数据加密失败') } } // 对称解密 function decrypt(encryptedData, key = process.env.ENCRYPTION_KEY) { try { const { encrypted, iv, tag } = encryptedData const decipher = crypto.createDecipher(ENCRYPTION_CONFIG.algorithm, key) decipher.setAuthTag(Buffer.from(tag, 'hex')) decipher.setAAD(Buffer.from('additional-data')) let decrypted = decipher.update(encrypted, 'hex', 'utf8') decrypted += decipher.final('utf8') return decrypted } catch (error) { logger.error('解密失败:', error) throw new Error('数据解密失败') } } // 密码哈希 async function hashPassword(password) { try { const salt = await bcrypt.genSalt(ENCRYPTION_CONFIG.saltRounds) return await bcrypt.hash(password, salt) } catch (error) { logger.error('密码哈希失败:', error) throw new Error('密码处理失败') } } // 密码验证 async function verifyPassword(password, hashedPassword) { try { return await bcrypt.compare(password, hashedPassword) } catch (error) { logger.error('密码验证失败:', error) return false } } // 敏感字段加密模型 class EncryptedField { constructor(value) { this.value = value } // 加密存储 encrypt() { if (!this.value) return null return JSON.stringify(encrypt(this.value)) } // 解密读取 static decrypt(encryptedValue) { if (!encryptedValue) return null try { const encryptedData = JSON.parse(encryptedValue) return decrypt(encryptedData) } catch (error) { logger.error('字段解密失败:', error) return null } } } // 数据库模型中使用加密字段 const User = sequelize.define('User', { username: DataTypes.STRING, email: DataTypes.STRING, phone: { type: DataTypes.TEXT, set(value) { if (value) { const encrypted = new EncryptedField(value) this.setDataValue('phone', encrypted.encrypt()) } }, get() { const encryptedValue = this.getDataValue('phone') return EncryptedField.decrypt(encryptedValue) } }, id_card: { type: DataTypes.TEXT, set(value) { if (value) { const encrypted = new EncryptedField(value) this.setDataValue('id_card', encrypted.encrypt()) } }, get() { const encryptedValue = this.getDataValue('id_card') return EncryptedField.decrypt(encryptedValue) } } }) ``` ## 📊 安全监控和审计 ### 安全事件日志 #### 日志记录系统 ```javascript // 安全事件类型 const SECURITY_EVENT_TYPES = { // 认证相关 LOGIN_SUCCESS: 'login_success', LOGIN_FAILED: 'login_failed', LOGOUT: 'logout', PASSWORD_CHANGED: 'password_changed', // 权限相关 PERMISSION_DENIED: 'permission_denied', ROLE_ASSIGNED: 'role_assigned', ROLE_REVOKED: 'role_revoked', // 安全威胁 SUSPICIOUS_ACTIVITY: 'suspicious_activity', BRUTE_FORCE_ATTEMPT: 'brute_force_attempt', SQL_INJECTION_ATTEMPT: 'sql_injection_attempt', XSS_ATTEMPT: 'xss_attempt', // 数据操作 DATA_ACCESS: 'data_access', DATA_MODIFICATION: 'data_modification', DATA_DELETION: 'data_deletion', // 系统事件 SYSTEM_ERROR: 'system_error', CONFIGURATION_CHANGED: 'configuration_changed' } // 安全事件日志模型 const SecurityLog = sequelize.define('SecurityLog', { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true }, event_type: { type: DataTypes.STRING, allowNull: false }, user_id: { type: DataTypes.INTEGER, allowNull: true }, ip_address: { type: DataTypes.STRING, allowNull: true }, user_agent: { type: DataTypes.TEXT, allowNull: true }, resource: { type: DataTypes.STRING, allowNull: true }, action: { type: DataTypes.STRING, allowNull: true }, details: { type: DataTypes.JSON, allowNull: true }, risk_level: { type: DataTypes.ENUM('low', 'medium', 'high', 'critical'), defaultValue: 'low' }, timestamp: { type: DataTypes.DATE, defaultValue: DataTypes.NOW } }) // 记录安全事件 async function logSecurityEvent(eventData) { try { const logEntry = await SecurityLog.create({ event_type: eventData.type, user_id: eventData.user_id, ip_address: eventData.ip_address, user_agent: eventData.user_agent, resource: eventData.resource, action: eventData.action, details: eventData.details, risk_level: eventData.risk_level || 'low', timestamp: eventData.timestamp || new Date() }) // 高风险事件立即告警 if (eventData.risk_level === 'high' || eventData.risk_level === 'critical') { await sendSecurityAlert(logEntry) } return logEntry } catch (error) { logger.error('安全事件记录失败:', error) } } // 安全事件分析 class SecurityAnalyzer { // 检测暴力破解攻击 static async detectBruteForceAttack(ip, timeWindow = 15 * 60 * 1000) { const since = new Date(Date.now() - timeWindow) const failedAttempts = await SecurityLog.count({ where: { event_type: SECURITY_EVENT_TYPES.LOGIN_FAILED, ip_address: ip, timestamp: { [Op.gte]: since } } }) if (failedAttempts >= 5) { await logSecurityEvent({ type: SECURITY_EVENT_TYPES.BRUTE_FORCE_ATTEMPT, ip_address: ip, details: { failed_attempts: failedAttempts }, risk_level: 'high' }) // 临时封禁IP await this.blockIP(ip, 60 * 60 * 1000) // 1小时 return true } return false } // 检测异常登录 static async detectAnomalousLogin(userId, currentIP, userAgent) { // 获取用户历史登录记录 const recentLogins = await SecurityLog.findAll({ where: { event_type: SECURITY_EVENT_TYPES.LOGIN_SUCCESS, user_id: userId, timestamp: { [Op.gte]: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } }, order: [['timestamp', 'DESC']], limit: 10 }) // 检查IP地址异常 const knownIPs = recentLogins.map(log => log.ip_address) const isNewIP = !knownIPs.includes(currentIP) // 检查设备异常 const knownUserAgents = recentLogins.map(log => log.user_agent) const isNewDevice = !knownUserAgents.includes(userAgent) if (isNewIP && isNewDevice) { await logSecurityEvent({ type: SECURITY_EVENT_TYPES.SUSPICIOUS_ACTIVITY, user_id: userId, ip_address: currentIP, user_agent: userAgent, details: { reason: 'new_ip_and_device', known_ips: knownIPs.slice(0, 3), known_devices: knownUserAgents.slice(0, 3) }, risk_level: 'medium' }) return true } return false } // 检测权限滥用 static async detectPrivilegeAbuse(userId, timeWindow = 60 * 60 * 1000) { const since = new Date(Date.now() - timeWindow) const privilegedActions = await SecurityLog.count({ where: { event_type: { [Op.in]: [ SECURITY_EVENT_TYPES.ROLE_ASSIGNED, SECURITY_EVENT_TYPES.DATA_MODIFICATION, SECURITY_EVENT_TYPES.DATA_DELETION ] }, user_id: userId, timestamp: { [Op.gte]: since } } }) // 如果1小时内特权操作超过阈值 if (privilegedActions > 20) { await logSecurityEvent({ type: SECURITY_EVENT_TYPES.SUSPICIOUS_ACTIVITY, user_id: userId, details: { reason: 'excessive_privileged_actions', action_count: privilegedActions }, risk_level: 'high' }) return true } return false } // IP封禁 static async blockIP(ip, duration) { const expiresAt = new Date(Date.now() + duration) await redis.setex( `blocked_ip:${ip}`, Math.ceil(duration / 1000), JSON.stringify({ blocked_at: new Date(), expires_at: expiresAt, reason: 'security_violation' }) ) logger.warn(`IP已被封禁: ${ip}, 到期时间: ${expiresAt}`) } // 检查IP是否被封禁 static async isIPBlocked(ip) { const blockData = await redis.get(`blocked_ip:${ip}`) return !!blockData } } // IP封禁检查中间件 function checkIPBlock(req, res, next) { return async (req, res, next) => { try { const isBlocked = await SecurityAnalyzer.isIPBlocked(req.ip) if (isBlocked) { await logSecurityEvent({ type: SECURITY_EVENT_TYPES.SUSPICIOUS_ACTIVITY, ip_address: req.ip, details: { reason: 'blocked_ip_access_attempt' }, risk_level: 'medium' }) return res.status(403).json({ error: '访问被拒绝', message: '您的IP地址已被临时封禁' }) } next() } catch (error) { logger.error('IP封禁检查失败:', error) next() } } } ``` ### 实时监控和告警 #### 安全告警系统 ```javascript // 告警配置 const ALERT_CONFIG = { channels: { email: { enabled: true, recipients: ['security@jiebanke.com', 'admin@jiebanke.com'] }, sms: { enabled: true, recipients: ['+8613800138000'] }, webhook: { enabled: true, url: 'https://hooks.slack.com/services/xxx' } }, thresholds: { failed_logins: 10, permission_denials: 20, suspicious_activities: 5 } } // 告警服务 class SecurityAlertService { // 发送安全告警 static async sendSecurityAlert(logEntry) { try { const alertData = { title: `安全告警 - ${this.getEventTypeName(logEntry.event_type)}`, message: this.formatAlertMessage(logEntry), severity: logEntry.risk_level, timestamp: logEntry.timestamp, details: logEntry } // 发送邮件告警 if (ALERT_CONFIG.channels.email.enabled) { await this.sendEmailAlert(alertData) } // 发送短信告警(仅高危事件) if (ALERT_CONFIG.channels.sms.enabled && ['high', 'critical'].includes(logEntry.risk_level)) { await this.sendSMSAlert(alertData) } // 发送Webhook告警 if (ALERT_CONFIG.channels.webhook.enabled) { await this.sendWebhookAlert(alertData) } logger.info(`安全告警已发送: ${logEntry.event_type}`) } catch (error) { logger.error('发送安全告警失败:', error) } } // 格式化告警消息 static formatAlertMessage(logEntry) { const messages = { [SECURITY_EVENT_TYPES.BRUTE_FORCE_ATTEMPT]: `检测到暴力破解攻击,IP: ${logEntry.ip_address}`, [SECURITY_EVENT_TYPES.SUSPICIOUS_ACTIVITY]: `检测到可疑活动,用户: ${logEntry.user_id}, IP: ${logEntry.ip_address}`, [SECURITY_EVENT_TYPES.SQL_INJECTION_ATTEMPT]: `检测到SQL注入攻击尝试,IP: ${logEntry.ip_address}`, [SECURITY_EVENT_TYPES.XSS_ATTEMPT]: `检测到XSS攻击尝试,IP: ${logEntry.ip_address}`, [SECURITY_EVENT_TYPES.PERMISSION_DENIED]: `权限拒绝事件,用户: ${logEntry.user_id}, 资源: ${logEntry.resource}` } return messages[logEntry.event_type] || `安全事件: ${logEntry.event_type}` } // 发送邮件告警 static async sendEmailAlert(alertData) { const emailContent = `

🚨 ${alertData.title}

时间: ${alertData.timestamp}

严重程度: ${alertData.severity}

描述: ${alertData.message}

详细信息:

${JSON.stringify(alertData.details, null, 2)}

请立即检查系统安全状况。

` await emailService.send({ to: ALERT_CONFIG.channels.email.recipients, subject: `[解班客安全告警] ${alertData.title}`, html: emailContent }) } // 发送短信告警 static async sendSMSAlert(alertData) { const message = `【解班客安全告警】${alertData.message},请立即处理。时间:${alertData.timestamp}` for (const recipient of ALERT_CONFIG.channels.sms.recipients) { await smsService.send({ to: recipient, message: message }) } } // 发送Webhook告警 static async sendWebhookAlert(alertData) { const payload = { text: `🚨 ${alertData.title}`, attachments: [{ color: this.getSeverityColor(alertData.severity), fields: [ { title: '时间', value: alertData.timestamp, short: true }, { title: '严重程度', value: alertData.severity, short: true }, { title: '描述', value: alertData.message, short: false } ] }] } await axios.post(ALERT_CONFIG.channels.webhook.url, payload) } // 获取严重程度颜色 static getSeverityColor(severity) { const colors = { low: '#36a64f', medium: '#ff9500', high: '#ff0000', critical: '#8b0000' } return colors[severity] || '#cccccc' } // 批量告警检查 static async checkBatchAlerts() { const timeWindow = 5 * 60 * 1000 // 5分钟 const since = new Date(Date.now() - timeWindow) // 检查失败登录次数 const failedLogins = await SecurityLog.count({ where: { event_type: SECURITY_EVENT_TYPES.LOGIN_FAILED, timestamp: { [Op.gte]: since } } }) if (failedLogins >= ALERT_CONFIG.thresholds.failed_logins) { await this.sendBatchAlert('大量登录失败', { count: failedLogins, timeWindow: '5分钟', threshold: ALERT_CONFIG.thresholds.failed_logins }) } // 检查权限拒绝次数 const permissionDenials = await SecurityLog.count({ where: { event_type: SECURITY_EVENT_TYPES.PERMISSION_DENIED, timestamp: { [Op.gte]: since } } }) if (permissionDenials >= ALERT_CONFIG.thresholds.permission_denials) { await this.sendBatchAlert('大量权限拒绝', { count: permissionDenials, timeWindow: '5分钟', threshold: ALERT_CONFIG.thresholds.permission_denials }) } } // 发送批量告警 static async sendBatchAlert(title, data) { const alertData = { title: `批量安全事件 - ${title}`, message: `在${data.timeWindow}内检测到${data.count}次${title}事件,超过阈值${data.threshold}`, severity: 'high', timestamp: new Date(), details: data } await this.sendSecurityAlert({ ...alertData, risk_level: 'high' }) } } // 定时检查批量告警 setInterval(async () => { try { await SecurityAlertService.checkBatchAlerts() } catch (error) { logger.error('批量告警检查失败:', error) } }, 5 * 60 * 1000) // 每5分钟检查一次 ``` ## 🔒 数据隐私保护 ### 数据脱敏 #### 敏感数据脱敏 ```javascript // 数据脱敏工具 class DataMasking { // 手机号脱敏 static maskPhone(phone) { if (!phone || phone.length < 11) return phone return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') } // 身份证号脱敏 static maskIDCard(idCard) { if (!idCard || idCard.length < 15) return idCard return idCard.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2') } // 邮箱脱敏 static maskEmail(email) { if (!email || !email.includes('@')) return email const [username, domain] = email.split('@') if (username.length <= 2) return email const maskedUsername = username.charAt(0) + '*'.repeat(username.length - 2) + username.charAt(username.length - 1) return `${maskedUsername}@${domain}` } // 姓名脱敏 static maskName(name) { if (!name || name.length <= 1) return name if (name.length === 2) { return name.charAt(0) + '*' } return name.charAt(0) + '*'.repeat(name.length - 2) + name.charAt(name.length - 1) } // 地址脱敏 static maskAddress(address) { if (!address || address.length <= 10) return address return address.substring(0, 6) + '****' + address.substring(address.length - 4) } // 银行卡号脱敏 static maskBankCard(cardNumber) { if (!cardNumber || cardNumber.length < 16) return cardNumber return cardNumber.replace(/(\d{4})\d{8}(\d{4})/, '$1********$2') } // 通用脱敏方法 static maskSensitiveData(data, fields) { const masked = { ...data } for (const field of fields) { if (masked[field]) { switch (field) { case 'phone': case 'mobile': masked[field] = this.maskPhone(masked[field]) break case 'id_card': case 'idCard': masked[field] = this.maskIDCard(masked[field]) break case 'email': masked[field] = this.maskEmail(masked[field]) break case 'name': case 'real_name': masked[field] = this.maskName(masked[field]) break case 'address': masked[field] = this.maskAddress(masked[field]) break case 'bank_card': masked[field] = this.maskBankCard(masked[field]) break default: // 默认脱敏:显示前2位和后2位 if (typeof masked[field] === 'string' && masked[field].length > 4) { masked[field] = masked[field].substring(0, 2) + '*'.repeat(masked[field].length - 4) + masked[field].substring(masked[field].length - 2) } } } } return masked } } // API响应脱敏中间件 function maskSensitiveResponse(sensitiveFields = []) { return (req, res, next) => { const originalJson = res.json res.json = function(data) { if (data && typeof data === 'object') { // 递归脱敏处理 const maskedData = maskDataRecursively(data, sensitiveFields) return originalJson.call(this, maskedData) } return originalJson.call(this, data) } next() } } // 递归脱敏处理 function maskDataRecursively(data, sensitiveFields) { if (Array.isArray(data)) { return data.map(item => maskDataRecursively(item, sensitiveFields)) } else if (data && typeof data === 'object') { return DataMasking.maskSensitiveData(data, sensitiveFields) } return data } ``` ### 数据备份和恢复 #### 安全备份策略 ```javascript // 备份配置 const BACKUP_CONFIG = { schedule: { full: '0 2 * * 0', // 每周日凌晨2点全量备份 incremental: '0 2 * * 1-6', // 周一到周六增量备份 log: '0 */6 * * *' // 每6小时备份日志 }, retention: { full: 30, // 保留30天 incremental: 7, // 保留7天 log: 3 // 保留3天 }, encryption: { enabled: true, algorithm: 'aes-256-cbc', keyRotation: 90 // 90天轮换密钥 }, storage: { local: '/backup/local', remote: 's3://jiebanke-backup', offsite: 'backup-server-2' } } // 备份服务 class BackupService { // 数据库全量备份 static async createFullBackup() { try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-') const backupName = `full-backup-${timestamp}` logger.info(`开始全量备份: ${backupName}`) // 1. 创建数据库备份 const dbBackupPath = await this.backupDatabase(backupName) // 2. 备份文件存储 const filesBackupPath = await this.backupFiles(backupName) // 3. 备份配置文件 const configBackupPath = await this.backupConfigs(backupName) // 4. 创建备份清单 const manifest = { backup_name: backupName, backup_type: 'full', created_at: new Date(), database: dbBackupPath, files: filesBackupPath, configs: configBackupPath, checksum: await this.calculateChecksum([dbBackupPath, filesBackupPath, configBackupPath]) } // 5. 加密备份 if (BACKUP_CONFIG.encryption.enabled) { await this.encryptBackup(manifest) } // 6. 上传到远程存储 await this.uploadToRemoteStorage(manifest) // 7. 记录备份日志 await this.logBackupEvent('FULL_BACKUP_SUCCESS', manifest) logger.info(`全量备份完成: ${backupName}`) return manifest } catch (error) { logger.error('全量备份失败:', error) await this.logBackupEvent('FULL_BACKUP_FAILED', { error: error.message }) throw error } } // 数据库备份 static async backupDatabase(backupName) { const backupPath = path.join(BACKUP_CONFIG.storage.local, `${backupName}-db.sql`) const command = `mysqldump -h ${process.env.DB_HOST} -u ${process.env.DB_USER} -p${process.env.DB_PASSWORD} ${process.env.DB_NAME} > ${backupPath}` await execAsync(command) // 验证备份文件 const stats = await fs.stat(backupPath) if (stats.size === 0) { throw new Error('数据库备份文件为空') } return backupPath } // 文件备份 static async backupFiles(backupName) { const backupPath = path.join(BACKUP_CONFIG.storage.local, `${backupName}-files.tar.gz`) const command = `tar -czf ${backupPath} ${process.env.UPLOAD_DIR} ${process.env.STATIC_DIR}` await execAsync(command) return backupPath } // 配置文件备份 static async backupConfigs(backupName) { const backupPath = path.join(BACKUP_CONFIG.storage.local, `${backupName}-configs.tar.gz`) const configDirs = [ './config', './docker-compose.yml', './package.json', './.env.example' ] const command = `tar -czf ${backupPath} ${configDirs.join(' ')}` await execAsync(command) return backupPath } // 增量备份 static async createIncrementalBackup() { try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-') const backupName = `incremental-backup-${timestamp}` logger.info(`开始增量备份: ${backupName}`) // 获取上次备份时间 const lastBackup = await this.getLastBackupTime() // 1. 增量数据库备份 const dbBackupPath = await this.backupDatabaseIncremental(backupName, lastBackup) // 2. 增量文件备份 const filesBackupPath = await this.backupFilesIncremental(backupName, lastBackup) // 3. 创建备份清单 const manifest = { backup_name: backupName, backup_type: 'incremental', created_at: new Date(), since: lastBackup, database: dbBackupPath, files: filesBackupPath, checksum: await this.calculateChecksum([dbBackupPath, filesBackupPath]) } // 4. 加密和上传 if (BACKUP_CONFIG.encryption.enabled) { await this.encryptBackup(manifest) } await this.uploadToRemoteStorage(manifest) await this.logBackupEvent('INCREMENTAL_BACKUP_SUCCESS', manifest) logger.info(`增量备份完成: ${backupName}`) return manifest } catch (error) { logger.error('增量备份失败:', error) await this.logBackupEvent('INCREMENTAL_BACKUP_FAILED', { error: error.message }) throw error } } // 备份恢复 static async restoreBackup(backupName, options = {}) { try { logger.info(`开始恢复备份: ${backupName}`) // 1. 下载备份文件 const manifest = await this.downloadBackup(backupName) // 2. 解密备份 if (BACKUP_CONFIG.encryption.enabled) { await this.decryptBackup(manifest) } // 3. 验证备份完整性 const isValid = await this.verifyBackupIntegrity(manifest) if (!isValid) { throw new Error('备份文件完整性验证失败') } // 4. 停止服务(如果需要) if (options.stopServices) { await this.stopServices() } // 5. 恢复数据库 if (options.restoreDatabase !== false) { await this.restoreDatabase(manifest.database) } // 6. 恢复文件 if (options.restoreFiles !== false) { await this.restoreFiles(manifest.files) } // 7. 恢复配置 if (options.restoreConfigs !== false && manifest.configs) { await this.restoreConfigs(manifest.configs) } // 8. 重启服务 if (options.stopServices) { await this.startServices() } await this.logBackupEvent('RESTORE_SUCCESS', { backup_name: backupName }) logger.info(`备份恢复完成: ${backupName}`) return true } catch (error) { logger.error('备份恢复失败:', error) await this.logBackupEvent('RESTORE_FAILED', { backup_name: backupName, error: error.message }) throw error } } // 备份清理 static async cleanupOldBackups() { try { const now = new Date() // 清理本地备份 const localBackups = await this.listLocalBackups() for (const backup of localBackups) { const age = (now - backup.created_at) / (1000 * 60 * 60 * 24) // 天数 let shouldDelete = false if (backup.type === 'full' && age > BACKUP_CONFIG.retention.full) { shouldDelete = true } else if (backup.type === 'incremental' && age > BACKUP_CONFIG.retention.incremental) { shouldDelete = true } else if (backup.type === 'log' && age > BACKUP_CONFIG.retention.log) { shouldDelete = true } if (shouldDelete) { await this.deleteBackup(backup) logger.info(`已删除过期备份: ${backup.name}`) } } // 清理远程备份 await this.cleanupRemoteBackups() } catch (error) { logger.error('备份清理失败:', error) } } } ``` ## 🔧 安全配置和部署 ### 服务器安全配置 #### Nginx安全配置 ```nginx # /etc/nginx/sites-available/jiebanke-security server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name api.jiebanke.com; # SSL配置 ssl_certificate /etc/letsencrypt/live/api.jiebanke.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.jiebanke.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # 安全头部 add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';" always; # 隐藏服务器信息 server_tokens off; # 限制请求大小 client_max_body_size 10M; client_body_buffer_size 128k; # 限制请求速率 limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s; # 防止缓冲区溢出 client_body_timeout 12; client_header_timeout 12; keepalive_timeout 15; send_timeout 10; # 主要API路由 location /api/ { limit_req zone=api burst=20 nodelay; # 代理到后端服务 proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; # 超时设置 proxy_connect_timeout 5s; proxy_send_timeout 10s; proxy_read_timeout 10s; } # 登录接口特殊限制 location /api/auth/login { limit_req zone=login burst=5 nodelay; proxy_pass http://127.0.0.1:3000; # ... 其他代理设置 } # 静态文件 location /uploads/ { alias /var/www/jiebanke/uploads/; expires 30d; add_header Cache-Control "public, immutable"; # 防止执行上传的脚本 location ~* \.(php|jsp|asp|sh|py|pl|exe)$ { deny all; } } # 禁止访问敏感文件 location ~ /\. { deny all; } location ~ \.(sql|log|conf)$ { deny all; } # 错误页面 error_page 404 /404.html; error_page 500 502 503 504 /50x.html; } # HTTP重定向到HTTPS server { listen 80; listen [::]:80; server_name api.jiebanke.com; return 301 https://$server_name$request_uri; } ``` #### 防火墙配置 ```bash #!/bin/bash # 防火墙安全配置脚本 # 清空现有规则 iptables -F iptables -X iptables -t nat -F iptables -t nat -X # 设置默认策略 iptables -P INPUT DROP iptables -P FORWARD DROP iptables -P OUTPUT ACCEPT # 允许本地回环 iptables -A INPUT -i lo -j ACCEPT iptables -A OUTPUT -o lo -j ACCEPT # 允许已建立的连接 iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # 允许SSH(限制连接数) iptables -A INPUT -p tcp --dport 22 -m connlimit --connlimit-above 3 -j DROP iptables -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT # 允许HTTP和HTTPS iptables -A INPUT -p tcp --dport 80 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -j ACCEPT # 防止DDoS攻击 iptables -A INPUT -p tcp --dport 80 -m limit --limit 25/minute --limit-burst 100 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -m limit --limit 25/minute --limit-burst 100 -j ACCEPT # 防止端口扫描 iptables -A INPUT -m recent --name portscan --rcheck --seconds 86400 -j DROP iptables -A INPUT -m recent --name portscan --remove iptables -A INPUT -p tcp -m tcp --dport 139 -m recent --name portscan --set -j LOG --log-prefix "Portscan:" iptables -A INPUT -p tcp -m tcp --dport 139 -j DROP # 防止SYN洪水攻击 iptables -A INPUT -p tcp --syn -m limit --limit 1/s --limit-burst 3 -j RETURN iptables -A INPUT -p tcp --syn -j DROP # 防止ping洪水攻击 iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/s -j ACCEPT iptables -A INPUT -p icmp --icmp-type echo-request -j DROP # 记录被丢弃的包 iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables denied: " --log-level 7 # 保存规则 iptables-save > /etc/iptables/rules.v4 ``` ### 环境变量安全管理 #### 密钥管理 ```javascript // 环境变量验证和管理 class EnvironmentManager { constructor() { this.requiredVars = [ 'NODE_ENV', 'PORT', 'DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME', 'JWT_SECRET', 'ENCRYPTION_KEY', 'REDIS_HOST', 'REDIS_PASSWORD' ] this.sensitiveVars = [ 'DB_PASSWORD', 'JWT_SECRET', 'ENCRYPTION_KEY', 'REDIS_PASSWORD', 'SMS_API_KEY', 'EMAIL_PASSWORD' ] } // 验证环境变量 validateEnvironment() { const missing = [] const weak = [] for (const varName of this.requiredVars) { const value = process.env[varName] if (!value) { missing.push(varName) continue } // 检查敏感变量强度 if (this.sensitiveVars.includes(varName)) { if (!this.isStrongSecret(value)) { weak.push(varName) } } } if (missing.length > 0) { throw new Error(`缺少必需的环境变量: ${missing.join(', ')}`) } if (weak.length > 0) { logger.warn(`以下环境变量强度不足: ${weak.join(', ')}`) } // 生产环境额外检查 if (process.env.NODE_ENV === 'production') { this.validateProductionEnvironment() } } // 检查密钥强度 isStrongSecret(secret) { if (secret.length < 32) return false const hasUpper = /[A-Z]/.test(secret) const hasLower = /[a-z]/.test(secret) const hasNumber = /\d/.test(secret) const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(secret) return hasUpper && hasLower && hasNumber && hasSpecial } // 生产环境验证 validateProductionEnvironment() { const productionChecks = { NODE_ENV: (val) => val === 'production', JWT_SECRET: (val) => val.length >= 64, ENCRYPTION_KEY: (val) => val.length >= 64, DB_SSL: (val) => val === 'true', REDIS_TLS: (val) => val === 'true' } for (const [varName, validator] of Object.entries(productionChecks)) { const value = process.env[varName] if (value && !validator(value)) { throw new Error(`生产环境配置错误: ${varName}`) } } } // 密钥轮换 async rotateSecrets() { try { logger.info('开始密钥轮换') // 生成新的JWT密钥 const newJwtSecret = this.generateStrongSecret(64) // 生成新的加密密钥 const newEncryptionKey = this.generateStrongSecret(64) // 更新密钥存储 await this.updateSecretStore({ JWT_SECRET: newJwtSecret, ENCRYPTION_KEY: newEncryptionKey, rotated_at: new Date().toISOString() }) // 记录轮换事件 await logSecurityEvent({ type: 'SECRET_ROTATION', details: { rotated_secrets: ['JWT_SECRET', 'ENCRYPTION_KEY'] }, risk_level: 'low' }) logger.info('密钥轮换完成') } catch (error) { logger.error('密钥轮换失败:', error) throw error } } // 生成强密钥 generateStrongSecret(length = 64) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*' let result = '' for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)) } return result } // 更新密钥存储 async updateSecretStore(secrets) { // 这里可以集成AWS Secrets Manager、HashiCorp Vault等 // 示例使用文件存储(生产环境不推荐) const secretsPath = '/etc/jiebanke/secrets.json' const existingSecrets = await this.loadSecrets() const updatedSecrets = { ...existingSecrets, ...secrets } await fs.writeFile(secretsPath, JSON.stringify(updatedSecrets, null, 2), { mode: 0o600 // 仅所有者可读写 }) } } // 初始化环境管理器 const envManager = new EnvironmentManager() envManager.validateEnvironment() // 定期密钥轮换(每90天) if (process.env.NODE_ENV === 'production') { setInterval(async () => { try { await envManager.rotateSecrets() } catch (error) { logger.error('自动密钥轮换失败:', error) } }, 90 * 24 * 60 * 60 * 1000) // 90天 } ``` ## 📚 总结 本安全和权限管理文档涵盖了解班客项目的完整安全体系,包括: ### 🎯 核心安全特性 - **多层安全防护**: 网络、应用、数据三层安全架构 - **身份认证系统**: JWT + MFA多因素认证 - **权限管理**: RBAC基于角色的访问控制 - **数据保护**: 加密存储、传输和脱敏处理 - **安全监控**: 实时威胁检测和告警系统 ### 🛡️ 安全防护措施 - **输入验证**: XSS、SQL注入、CSRF防护 - **速率限制**: API访问频率控制 - **数据加密**: 敏感信息加密存储 - **安全备份**: 定期备份和恢复机制 - **环境安全**: 密钥管理和轮换 ### 📊 监控和审计 - **安全日志**: 完整的操作审计跟踪 - **威胁检测**: 自动化安全威胁识别 - **告警系统**: 多渠道安全事件通知 - **合规性**: 符合数据保护法规要求 通过实施这套完整的安全体系,解班客项目能够有效防范各类安全威胁,保护用户数据安全,确保系统稳定可靠运行。