66 KiB
66 KiB
解班客安全和权限管理文档
📋 概述
本文档详细描述解班客项目的安全架构、权限管理体系、安全防护措施和安全最佳实践。通过多层次的安全防护,确保系统和用户数据的安全性。
🎯 安全目标
核心安全原则
- 最小权限原则: 用户和系统组件仅获得完成任务所需的最小权限
- 深度防御: 多层安全防护,避免单点失效
- 零信任架构: 不信任任何内部或外部实体,持续验证
- 数据保护: 全生命周期数据安全保护
- 合规性: 符合相关法律法规和行业标准
安全目标
- 身份认证: 确保用户身份的真实性和唯一性
- 访问控制: 基于角色和权限的精细化访问控制
- 数据安全: 敏感数据加密存储和传输
- 系统安全: 防范各类网络攻击和安全威胁
- 审计追踪: 完整的操作日志和安全审计
🏗️ 安全架构
整体安全架构
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访问频率控制
- 数据加密: 敏感信息加密存储
- 安全备份: 定期备份和恢复机制
- 环境安全: 密钥管理和轮换
📊 监控和审计
- 安全日志: 完整的操作审计跟踪
- 威胁检测: 自动化安全威胁识别
- 告警系统: 多渠道安全事件通知
- 合规性: 符合数据保护法规要求
通过实施这套完整的安全体系,解班客项目能够有效防范各类安全威胁,保护用户数据安全,确保系统稳定可靠运行。