Files
jiebanke/docs/安全和权限管理文档.md

66 KiB
Raw Blame History

解班客安全和权限管理文档

📋 概述

本文档详细描述解班客项目的安全架构、权限管理体系、安全防护措施和安全最佳实践。通过多层次的安全防护,确保系统和用户数据的安全性。

🎯 安全目标

核心安全原则

  • 最小权限原则: 用户和系统组件仅获得完成任务所需的最小权限
  • 深度防御: 多层安全防护,避免单点失效
  • 零信任架构: 不信任任何内部或外部实体,持续验证
  • 数据保护: 全生命周期数据安全保护
  • 合规性: 符合相关法律法规和行业标准

安全目标

  • 身份认证: 确保用户身份的真实性和唯一性
  • 访问控制: 基于角色和权限的精细化访问控制
  • 数据安全: 敏感数据加密存储和传输
  • 系统安全: 防范各类网络攻击和安全威胁
  • 审计追踪: 完整的操作日志和安全审计

🏗️ 安全架构

整体安全架构

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结构

// 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"
}

认证流程

// 用户登录认证
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)

短信验证码

// 发送短信验证码
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认证器

// 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权限模型

权限数据模型

-- 角色表
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)
);

权限检查中间件

// 权限检查中间件
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 []
  }
}

角色管理

预定义角色

// 系统预定义角色
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
  }
}

动态权限管理

// 权限管理服务
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注入防护

// 使用参数化查询防止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防护

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\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
    /javascript:/gi,
    /on\w+\s*=/gi,
    /eval\s*\(/gi,
    /expression\s*\(/gi
  ]
  
  return dangerousPatterns.some(pattern => pattern.test(input))
}

CSRF防护

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速率限制

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)
    )
  }
}

数据加密

敏感数据加密

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)
    }
  }
})

📊 安全监控和审计

安全事件日志

日志记录系统

// 安全事件类型
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()
    }
  }
}

实时监控和告警

安全告警系统

// 告警配置
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 = `
      <h2>🚨 ${alertData.title}</h2>
      <p><strong>时间:</strong> ${alertData.timestamp}</p>
      <p><strong>严重程度:</strong> ${alertData.severity}</p>
      <p><strong>描述:</strong> ${alertData.message}</p>
      
      <h3>详细信息:</h3>
      <pre>${JSON.stringify(alertData.details, null, 2)}</pre>
      
      <p>请立即检查系统安全状况。</p>
    `
    
    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分钟检查一次

🔒 数据隐私保护

数据脱敏

敏感数据脱敏

// 数据脱敏工具
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
}

数据备份和恢复

安全备份策略

// 备份配置
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安全配置

# /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;
}

防火墙配置

#!/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

环境变量安全管理

密钥管理

// 环境变量验证和管理
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访问频率控制
  • 数据加密: 敏感信息加密存储
  • 安全备份: 定期备份和恢复机制
  • 环境安全: 密钥管理和轮换

📊 监控和审计

  • 安全日志: 完整的操作审计跟踪
  • 威胁检测: 自动化安全威胁识别
  • 告警系统: 多渠道安全事件通知
  • 合规性: 符合数据保护法规要求

通过实施这套完整的安全体系,解班客项目能够有效防范各类安全威胁,保护用户数据安全,确保系统稳定可靠运行。