Skip to content

第二十章 安全机制深度解析

作者

谭策 — 独立开发者 | AIOps 领域探索者

IT Online 微信公众号

许可证

MPL-2.0 © 谭策

本章导读

ITOps Agent Platform 作为企业级 IT 运维自动化平台,直接管理服务器、执行命令、处理告警,安全是其核心设计原则之一。本章将从认证授权、数据加密、请求防护、命令安全、审计追踪五个维度,深入剖析项目中的安全机制实现。通过本章学习,你将理解如何在 Node.js + Express 后端中构建多层次的安全防护体系。

学习目标

  • 掌握 JWT 双 Token(Access Token + Refresh Token)认证流程
  • 理解 Token 黑名单服务的实现原理与内存缓存策略
  • 掌握 AES-256-GCM 加密服务及其密钥轮换机制
  • 理解 bcrypt 密码哈希与 Helmet 安全头配置
  • 掌握速率限制中间件和命令过滤策略
  • 理解审计日志系统的设计与 CORS 配置

核心内容

20.1 JWT 双 Token 认证流程

项目采用 Access Token + Refresh Token 双 Token 机制,兼顾安全性和用户体验。

Token 生命周期:

┌──────────────────────────────────────────────────────────────┐
│                    双 Token 认证流程                          │
│                                                              │
│   ┌──────────┐   登录成功    ┌─────────────────────────────┐ │
│   │  客户端   │─────────────▶│  Access Token (24h)         │ │
│   │          │              │  Refresh Token (7d)         │ │
│   └────┬─────┘              └─────────────────────────────┘ │
│        │                                                     │
│        │  每次请求携带 Access Token                          │
│        ▼                                                     │
│   ┌──────────┐                                              │
│   │  验证    │──── 过期 ────▶ 使用 Refresh Token 请求 /refresh│
│   │  Token   │                     │                        │
│   └────┬─────┘                     ▼                        │
│        │                    ┌──────────────────┐            │
│        │ 有效               │ 签发新 Access    │            │
│        ▼                    │ Token + 新       │            │
│   ┌──────────┐              │ Refresh Token    │            │
│   │ 放行请求  │              │ (旧 Refresh 作废) │            │
│   └──────────┘              └──────────────────┘            │
└──────────────────────────────────────────────────────────────┘

登录接口生成 Token:

typescript
// backend/src/routes/authRoutes.ts
router.post('/login', validateBody(authSchemas.login), async (req, res) => {
  const { username, password } = req.body;

  // 查询用户
  const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);

  if (!user || !user.enabled) {
    return res.status(401).json({ success: false, message: '用户名或密码错误' });
  }

  // bcrypt 密码比对
  const validPassword = await bcrypt.compare(password, user.password);
  if (!validPassword) {
    return res.status(401).json({ success: false, message: '用户名或密码错误' });
  }

  // 生成 Access Token (24小时有效)
  const accessToken = jwt.sign(
    { id: user.id, username: user.username, role: user.role, email: user.email },
    env.JWT_SECRET,
    { expiresIn: env.JWT_EXPIRES_IN }   // 默认 24h
  );

  // 生成 Refresh Token (7天有效)
  const refreshToken = jwt.sign(
    { id: user.id, type: 'refresh' },
    env.JWT_SECRET,
    { expiresIn: '7d' }
  );

  // 记录审计日志
  db.prepare(`
    INSERT INTO audit_logs (id, user_id, action, resource_type, resource_id, details, ip_address)
    VALUES (?, ?, 'login', 'auth', 'login', ?, ?, CURRENT_TIMESTAMP)
  `).run(randomUUID(), user.id, JSON.stringify({ username }), req.ip);

  res.json({ success: true, data: { token: accessToken, refreshToken, user } });
});

认证中间件:

typescript
// backend/src/middleware/auth.ts
export function authenticateToken(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ success: false, message: '未提供认证token' });
  }

  const token = authHeader.substring(7);

  // 检查黑名单
  if (tokenBlacklist.isBlacklisted(token)) {
    return res.status(401).json({ success: false, message: 'Token已失效' });
  }

  try {
    // 验证 JWT 签名和过期时间
    const decoded = jwt.verify(token, env.JWT_SECRET, { algorithms: ['HS256'] });

    // 从缓存或数据库获取用户信息
    let user = getCachedUser(decoded.id);
    if (!user) {
      user = db.prepare('SELECT id, username, email, role, enabled FROM users WHERE id = ?').get(decoded.id);
      if (user) setCachedUser(decoded.id, user);
    }

    if (!user || !user.enabled) {
      return res.status(403).json({ success: false, message: '账户已被禁用' });
    }

    req.user = user;
    next();
  } catch (error) {
    if (error instanceof jwt.TokenExpiredError) {
      return res.status(401).json({ success: false, message: 'Token已过期' });
    }
    return res.status(401).json({ success: false, message: '无效的token' });
  }
}

Token 刷新机制(Rotation):

typescript
// backend/src/routes/authRoutes.ts
router.post('/refresh', async (req, res) => {
  const { refreshToken } = req.body;

  // 检查黑名单
  if (tokenBlacklist.isBlacklisted(refreshToken)) {
    return res.status(401).json({ success: false, message: 'Token已失效' });
  }

  // 验证 Refresh Token
  const decoded = jwt.verify(refreshToken, env.JWT_SECRET, { algorithms: ['HS256'] });

  if (decoded.type !== 'refresh') {
    return res.status(401).json({ success: false, message: '无效的refresh token' });
  }

  // 签发新 Token 对
  const newAccessToken = jwt.sign(
    { id: decoded.id, username, role, email },
    env.JWT_SECRET,
    { expiresIn: env.JWT_EXPIRES_IN }
  );

  const newRefreshToken = jwt.sign(
    { id: decoded.id, type: 'refresh' },
    env.JWT_SECRET,
    { expiresIn: '7d' }
  );

  // 旧 Refresh Token 加入黑名单(防重放攻击)
  tokenBlacklist.addToBlacklist(refreshToken, 'token-refresh', decoded.id);

  res.json({ success: true, data: { token: newAccessToken, refreshToken: newRefreshToken } });
});

安全要点:

机制实现防护目标
算法锁定{ algorithms: ['HS256'] }防止算法切换攻击
Token 黑名单数据库 + 内存缓存双写支持主动撤销
Refresh Token 轮换旧 Token 立即作废防重放攻击
用户状态校验每次验证检查 enabled即时禁用账户
用户信息缓存TTL 60 秒内存缓存减少数据库查询

20.2 Token 黑名单服务

Token 黑名单服务解决了 JWT 无状态特性带来的撤销难题。

typescript
// backend/src/services/tokenBlacklist.ts
const blacklistedTokenCache = new Map<string, CachedToken>();
const CACHE_CLEANUP_INTERVAL = 10 * 60 * 1000; // 10 分钟
const MAX_CACHE_SIZE = 10000;

class TokenBlacklistService {
  addToBlacklist(token: string, reason?: string, userId?: string): void {
    // 解析 Token 过期时间
    const decoded = jwt.decode(token) as { exp?: number } | null;
    const expiresAt = decoded?.exp
      ? new Date(decoded.exp * 1000)
      : new Date(Date.now() + 24 * 60 * 60 * 1000);

    // 写入内存缓存
    blacklistedTokenCache.set(token, { token, expiresAt });
    this.enforceCacheLimit();

    // 写入数据库(持久化)
    db.prepare(`
      INSERT OR IGNORE INTO token_blacklist (id, token, user_id, reason, expires_at)
      VALUES (?, ?, ?, ?, ?)
    `).run(randomUUID(), token, userId, reason, expiresAt.toISOString());
  }

  isBlacklisted(token: string): boolean {
    // 优先查缓存(O(1))
    const cached = blacklistedTokenCache.get(token);
    if (cached && cached.expiresAt > new Date()) return true;
    if (cached) blacklistedTokenCache.delete(token);

    // 查数据库
    const result = db.prepare(`
      SELECT 1 FROM token_blacklist
      WHERE token = ? AND expires_at > CURRENT_TIMESTAMP
    `).get(token);

    const isBlacklisted = !!result;
    if (isBlacklisted) {
      // 回填缓存
      blacklistedTokenCache.set(token, { token, expiresAt: ... });
    }
    return isBlacklisted;
  }
}

双层存储架构:

请求验证 isBlacklisted(token)


  ┌──────────────┐
  │  内存 Map    │ ◀── O(1) 查找,~100ns
  │  (最多10000) │
  │              │ ── 命中 ──▶ 返回结果
  │              │
  │  ── 未命中 ──│──▶ 清理过期缓存
  └──────────────┘


  ┌──────────────┐
  │  SQLite 表   │ ◀── 索引查找,~1ms
  │  token_      │
  │  blacklist   │ ── 找到 ──▶ 回填缓存 ──▶ 返回 true
  │              │
  │  ── 未找到 ──│──────────────────────▶ 返回 false
  └──────────────┘

缓存容量管理:

策略触发条件操作
过期清理定期每 10 分钟删除所有过期条目
软限制超过 10000 条清理过期 + 删除最早的一半
单次写入检查每次 addToBlacklist检查并强制限制

20.3 AES-256-GCM 加密服务

服务器密码和 SSH 私钥等敏感数据使用 AES-256-GCM 加密存储。

typescript
// backend/src/services/encryptionService.ts
import crypto from 'crypto';

const ALGORITHM = 'aes-256-gcm';
const KEY_LENGTH = 32;  // 256 bits
const IV_LENGTH = 16;   // 128 bits

// 加密密钥管理:存储在数据库中,支持轮换
function getOrCreateEncryptionKey(): Buffer {
  const activeKey = db.prepare(
    'SELECT key_value FROM encryption_keys WHERE key_type = ? AND active = 1 LIMIT 1'
  ).get('aes-256-gcm');

  if (!activeKey) {
    const newKey = crypto.randomBytes(KEY_LENGTH);
    db.prepare(`
      INSERT INTO encryption_keys (id, key_type, key_value, active)
      VALUES (?, ?, ?, 1)
    `).run(randomUUID(), 'aes-256-gcm', newKey.toString('base64'));
    return newKey;
  }

  return Buffer.from(activeKey.key_value, 'base64');
}

// 加密:返回 iv:authTag:encryptedData 格式
export function encrypt(plaintext: string): string {
  const iv = crypto.randomBytes(IV_LENGTH);
  const cipher = crypto.createCipheriv(ALGORITHM, getEncryptionKey(), iv);

  let encrypted = cipher.update(plaintext, 'utf8', 'base64');
  encrypted += cipher.final('base64');

  const authTag = cipher.getAuthTag();

  return `${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted}`;
}

// 解密
export function decrypt(encryptedString: string): string {
  const parts = encryptedString.split(':');
  if (parts.length !== 3) throw new Error('Invalid encrypted data format');

  const iv = Buffer.from(parts[0], 'base64');
  const authTag = Buffer.from(parts[1], 'base64');
  const encryptedData = Buffer.from(parts[2], 'base64');

  const decipher = crypto.createDecipheriv(ALGORITHM, getEncryptionKey(), iv);
  decipher.setAuthTag(authTag);

  let decrypted = decipher.update(encryptedData);
  decrypted = Buffer.concat([decrypted, decipher.final()]);

  return decrypted.toString('utf8');
}

加密数据格式:

iv:authTag:encryptedData
│  │        │
│  │        └── Base64 编码的密文 (AES-256-GCM)
│  │
│  └── Base64 编码的认证标签 (16字节,防篡改)

└── Base64 编码的初始化向量 (16字节,随机)

密钥轮换机制:

typescript
export function rotateEncryptionKey(): void {
  const newKey = crypto.randomBytes(KEY_LENGTH);

  // 获取所有服务器数据
  const servers = db.prepare('SELECT id, password, private_key FROM servers').all();

  // 用旧密钥解密
  const oldKey = getEncryptionKey();
  const decrypted = servers.map(s => ({
    id: s.id,
    password: s.password ? decryptWithKey(s.password, oldKey) : null,
    private_key: s.private_key ? decryptWithKey(s.private_key, oldKey) : null
  }));

  // 事务包装:原子性保证
  const tx = db.transaction(() => {
    db.prepare('UPDATE encryption_keys SET active = 0 WHERE key_type = ?').run('aes-256-gcm');
    db.prepare('INSERT INTO encryption_keys ... VALUES (?, ?, ?, 1)')
      .run(randomUUID(), 'aes-256-gcm', newKey.toString('base64'));

    // 用新密钥重新加密
    for (const s of decrypted) {
      updateStmt.run(
        s.password ? encryptWithKey(s.password, newKey) : null,
        s.private_key ? encryptWithKey(s.private_key, newKey) : null,
        s.id
      );
    }
  });

  tx();
}

AES-256-GCM vs 其他加密模式:

特性GCMCBCCTR
认证标签内置 (AEAD)无,需额外 HMAC无,需额外 HMAC
并行加密
防篡改自动验证需手动需手动
安全性最高需正确填充中等
推荐度首选不推荐不推荐

20.4 bcrypt 密码哈希

用户密码使用 bcrypt 算法进行哈希存储。

typescript
// 注册/创建用户时
const hashedPassword = await bcrypt.hash(plaintextPassword, 12);
// cost factor = 12,约 250ms 计算时间

// 登录时验证
const validPassword = await bcrypt.compare(plaintextPassword, hashedPassword);

bcrypt 参数说明:

$2b$12$LJ3m4ys3Lk8fGxG5ZqY0.eO0z3F3xM0kL8fGxG5ZqY0.eO0z3F3xM
 │  │  │
 │  │  └── 22 字符盐 + 31 字符哈希值
 │  │
 │  └── cost factor = 12 (2^12 = 4096 轮迭代)

 └── bcrypt 版本标识

为什么选择 cost factor = 12:

Cost Factor计算时间 (2024年硬件)破解速度 (每秒尝试)推荐场景
8~20ms~50次/s过低,不推荐
10~80ms~12次/s最低推荐
12~250ms~4次/s生产推荐
14~1s~1次/s极高安全要求
16~4s~0.25次/s用户体验差

20.5 Helmet 安全头

Helmet 中间件自动设置一系列 HTTP 安全响应头。

typescript
// backend/src/app.ts
import helmet from 'helmet';

app.use(helmet());

Helmet 设置的安全头:

响应头防护目标
Content-Security-Policydefault-src 'self'限制资源加载来源,防 XSS
X-Content-Type-Optionsnosniff防止 MIME 类型嗅探
X-Frame-OptionsSAMEORIGIN防止点击劫持 (iframe 嵌入)
X-DNS-Prefetch-Controloff禁用 DNS 预取
Strict-Transport-Securitymax-age=...强制 HTTPS (HSTS)
X-Download-OptionsnoopenIE 阻止自动下载执行
X-Permitted-Cross-Domain-Policiesnone禁止跨域策略文件
Referrer-Policyno-referrer控制 Referer 信息泄露
X-XSS-Protection0禁用旧版 XSS 过滤器

Nginx 侧的额外安全头:

nginx
# docker/nginx.conf
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" 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'; ..." always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
server_tokens off;  # 隐藏 Nginx 版本号

20.6 速率限制中间件

自定义内存级速率限制,针对不同路由实施差异化策略。

typescript
// backend/src/middleware/rateLimiter.ts
const rateLimitConfig: RateLimitConfig = {
  '/api/auth/login':        { windowMs: 15 * 60 * 1000, max: 5 },    // 登录:15分钟5次
  '/api/auth':              { windowMs: 60 * 1000, max: 20 },        // 认证接口:每分钟20次
  '/api/copilot':           { windowMs: 60 * 1000, max: 30 },        // Copilot:每分钟30次
  '/api/settings/api-keys': { windowMs: 60 * 1000, max: 10 },        // API密钥:每分钟10次
  '/api/webhooks':          { windowMs: 1000, max: 10 },             // Webhook:每秒10次
};

const DEFAULT_CONFIG = { windowMs: 60 * 1000, max: 100 };             // 默认:每分钟100次

export function rateLimiter(req: Request, res: Response, next: NextFunction) {
  // 查找匹配的路由配置
  let config = DEFAULT_CONFIG;
  for (const [path, cfg] of Object.entries(rateLimitConfig)) {
    if (req.path.startsWith(path)) { config = cfg; break; }
  }

  // 以 IP + Method + Path 作为限流键
  const key = `${req.ip}:${req.method}:${req.path}`;
  const now = Date.now();

  let entry = rateLimitStore.get(key);
  if (!entry || now > entry.resetTime) {
    cleanupRateLimitStore();
    entry = { count: 0, resetTime: now + config.windowMs };
    rateLimitStore.set(key, entry);
  }

  entry.count++;

  // 设置限流响应头
  res.setHeader('X-RateLimit-Limit', config.max.toString());
  res.setHeader('X-RateLimit-Remaining', Math.max(0, config.max - entry.count).toString());
  res.setHeader('X-RateLimit-Reset', Math.ceil(entry.resetTime / 1000).toString());

  if (entry.count > config.max) {
    return res.status(429).json({
      success: false,
      message: '请求过于频繁,请稍后再试',
      retryAfter: Math.ceil((entry.resetTime - now) / 1000)
    });
  }

  next();
}

限流策略表:

路由时间窗口最大请求防护目标
/api/auth/login15 分钟5 次防止暴力破解密码
/api/auth/*1 分钟20 次防止认证接口滥用
/api/copilot1 分钟30 次防止 LLM API 费用失控
/api/settings/api-keys1 分钟10 次防止密钥频繁操作
/api/webhooks1 秒10 次允许批量告警推送
其他路由1 分钟100 次通用防护

20.7 SSH 命令过滤

命令过滤中间件基于正则表达式匹配危险命令,按用户角色实施不同级别的管控。

typescript
// backend/src/middleware/commandFilter.ts
interface CommandPolicy {
  name: string;
  description: string;
  patterns: RegExp[];
  action: 'block' | 'warn' | 'allow';
  blockedRoles: string[];
}

const DANGEROUS_COMMANDS: CommandPolicy[] = [
  {
    name: 'filesystem_destructive',
    description: '破坏性文件系统操作',
    patterns: [
      /^rm\s+-rf\s+\/{1,3}$/,   // rm -rf /
      /^rm\s+-rf\s+\*$/,         // rm -rf *
      /^>\s*\/dev\/sd/,          // 覆盖磁盘设备
      /^dd\s+if=\/dev\/zero/,    // dd 写零
      /^shred\s+-/,              // 安全擦除
    ],
    action: 'block',
    blockedRoles: ['viewer', 'operator'],
  },
  {
    name: 'system_critical',
    description: '系统关键操作',
    patterns: [
      /^mkfs\./,                 // 格式化文件系统
      /^fdisk\s/,                // 磁盘分区
      /^cryptsetup\s/,           // 加密设备
    ],
    action: 'block',
    blockedRoles: ['viewer', 'operator', 'admin'],  // admin 也禁止
  },
  {
    name: 'credential_access',
    description: '凭据访问尝试',
    patterns: [
      /\/etc\/shadow/,           // 读取密码文件
      /cat\s+.*id_rsa/,          // 读取 SSH 私钥
      /export\s+.*PASSWORD/,     // 导出密码变量
    ],
    action: 'warn',
    blockedRoles: ['viewer'],
  },
];

export function checkCommandSafety(command: string, userRole: string): {
  allowed: boolean;
  severity: 'blocked' | 'warning' | 'safe';
  reason?: string;
  policy?: string;
} {
  const trimmed = command.trim();

  for (const policy of DANGEROUS_COMMANDS) {
    for (const pattern of policy.patterns) {
      if (pattern.test(trimmed)) {
        if (policy.action === 'block' && policy.blockedRoles.includes(userRole)) {
          return { allowed: false, severity: 'blocked', reason: policy.description, policy: policy.name };
        }
        if (policy.action === 'warn' && policy.blockedRoles.includes(userRole)) {
          return { allowed: true, severity: 'warning', reason: policy.description, policy: policy.name };
        }
        break;
      }
    }
  }

  return { allowed: true, severity: 'safe' };
}

命令过滤决策树:

用户输入命令


  trim() 去除首尾空白


  遍历策略列表

     ├── 匹配 filesystem_destructive
     │   ├── viewer/operator → BLOCK
     │   └── admin → 放行

     ├── 匹配 system_critical
     │   ├── viewer/operator/admin → BLOCK (全部禁止)

     ├── 匹配 credential_access
     │   ├── viewer → WARN (允许但告警)
     │   └── operator/admin → 放行

     └── 无匹配 → SAFE (放行)

角色与权限矩阵:

操作类型vieweroperatoradmin
只读命令允许允许允许
破坏性文件操作拦截拦截允许
系统关键操作拦截拦截拦截
凭据访问告警允许允许
权限提升告警允许允许

20.8 审计日志系统

所有关键操作均记录审计日志,支持追溯和安全合规。

sql
-- 审计日志表结构
CREATE TABLE audit_logs (
  id TEXT PRIMARY KEY,
  user_id TEXT,                  -- 操作用户
  action TEXT NOT NULL,          -- 操作类型
  resource_type TEXT,            -- 资源类型
  resource_id TEXT,              -- 资源 ID
  details TEXT,                  -- 操作详情 (JSON)
  ip_address TEXT,               -- 客户端 IP
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_audit_user ON audit_logs(user_id);
CREATE INDEX idx_audit_created_at ON audit_logs(created_at);

记录的审计事件:

事件actionresource_type记录内容
用户登录loginauth用户名、IP
用户登出logoutauth用户名
修改密码change_passwordauth用户名
创建服务器create_serverserver服务器名称、IP
执行命令execute_commandserver命令内容、结果
创建 Agentcreate_agentagentAgent 名称
执行任务execute_tasktask任务 ID、工作流
密钥轮换rotate_keysecurity旧密钥 ID

20.9 CORS 配置

跨域资源共享 (CORS) 通过环境变量灵活控制。

typescript
// backend/src/app.ts
import cors from 'cors';

app.use(cors({
  origin: env.ALLOWED_ORIGINS,   // 从环境变量读取
  credentials: true              // 允许携带 Cookie
}));

// WebSocket CORS
const io = new SocketIOServer(httpServer, {
  cors: {
    origin: env.ALLOWED_ORIGINS,
    methods: ['GET', 'POST']
  },
  maxHttpBufferSize: 1e6,        // 1MB 限制
  pingTimeout: 60000,
  pingInterval: 25000
});
typescript
// backend/src/utils/env.ts
ALLOWED_ORIGINS: getEnv('ALLOWED_ORIGINS', 'http://localhost:3000')
  .split(',')
  .map(s => s.trim())

生产环境 CORS 配置示例:

bash
# 开发环境(宽松)
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173

# 生产环境(严格)
ALLOWED_ORIGINS=https://your-domain.com

# Docker Compose 环境
ALLOWED_ORIGINS=http://localhost:80,http://itops-frontend,http://frontend

本章小结

本章深入剖析了 ITOps Agent Platform 的多层次安全防护体系:

  1. 认证层:JWT 双 Token 机制 + Token 黑名单实现安全的无状态认证
  2. 加密层:AES-256-GCM 加密敏感数据 + bcrypt 哈希密码
  3. 防护层:Helmet 安全头 + 速率限制 + 命令过滤
  4. 审计层:全操作日志记录,支持追溯和合规
  5. 网络层:CORS 白名单 + WebSocket 连接管控

这些安全机制相互配合,构成了纵深防御体系。理解并掌握这些机制,将帮助你在实际运维中确保平台的安全性。

本章练习

基础练习

  1. 实现 Token 过期自动刷新:在前端封装一个 HTTP 客户端(使用 Axios 或 Fetch),在 Access Token 过期时自动使用 Refresh Token 请求新 Token,并重放原始请求。

  2. 添加 IP 黑名单功能:在现有的 rateLimiter.ts 基础上,扩展实现 IP 黑名单功能。当某个 IP 在 1 小时内触发 429 超过 10 次时,自动将其加入黑名单,拒绝所有请求。

  3. 密码强度校验:在注册和修改密码时,增加密码强度校验:至少 8 位、包含大小写字母、数字和特殊字符。编写对应的正则表达式和中间件。

进阶练习

  1. 实现双因素认证 (2FA):使用 TOTP (Time-based One-Time Password) 算法实现双因素认证。用户首次登录时生成 QR 码,后续登录需要输入 6 位动态验证码。

  2. 实现 RBAC 权限模型:在现有的 requireRole 基础上,设计基于角色的访问控制 (RBAC) 系统。定义角色-权限-资源的三层关系,支持动态配置。

  3. 实现 API 签名验证:为 Webhook 接口实现 HMAC-SHA256 签名验证,参考项目中已有的 verifyWebhookSignature 函数,确保外部推送的告警数据未被篡改。

思考题

  1. 项目使用内存中的 Map 实现速率限制和 Token 黑名单。在分布式部署(多实例)场景下,这些机制会存在什么问题?如何基于 Redis 实现分布式限流和黑名单?

  2. AES-256-GCM 的密钥存储在 SQLite 数据库中。如果攻击者获得了数据库文件的访问权限,加密是否仍然有效?请分析"加密的密钥与数据同源"这一架构的安全风险,并提出改进方案。

延伸阅读

基于 MPL-2.0 许可证发布