第二十章 安全机制深度解析
作者
谭策 — 独立开发者 | AIOps 领域探索者
- 🌐 项目官网:ITOpsAgentinfo
- 📝 博客:zjzwfw.cloud
- 📧 邮箱:huawei_network@foxmail.com
- 💬 微信公众号: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:
// 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 } });
});认证中间件:
// 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):
// 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 无状态特性带来的撤销难题。
// 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 加密存储。
// 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字节,随机)密钥轮换机制:
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 其他加密模式:
| 特性 | GCM | CBC | CTR |
|---|---|---|---|
| 认证标签 | 内置 (AEAD) | 无,需额外 HMAC | 无,需额外 HMAC |
| 并行加密 | 是 | 否 | 是 |
| 防篡改 | 自动验证 | 需手动 | 需手动 |
| 安全性 | 最高 | 需正确填充 | 中等 |
| 推荐度 | 首选 | 不推荐 | 不推荐 |
20.4 bcrypt 密码哈希
用户密码使用 bcrypt 算法进行哈希存储。
// 注册/创建用户时
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 安全响应头。
// backend/src/app.ts
import helmet from 'helmet';
app.use(helmet());Helmet 设置的安全头:
| 响应头 | 值 | 防护目标 |
|---|---|---|
Content-Security-Policy | default-src 'self' | 限制资源加载来源,防 XSS |
X-Content-Type-Options | nosniff | 防止 MIME 类型嗅探 |
X-Frame-Options | SAMEORIGIN | 防止点击劫持 (iframe 嵌入) |
X-DNS-Prefetch-Control | off | 禁用 DNS 预取 |
Strict-Transport-Security | max-age=... | 强制 HTTPS (HSTS) |
X-Download-Options | noopen | IE 阻止自动下载执行 |
X-Permitted-Cross-Domain-Policies | none | 禁止跨域策略文件 |
Referrer-Policy | no-referrer | 控制 Referer 信息泄露 |
X-XSS-Protection | 0 | 禁用旧版 XSS 过滤器 |
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 速率限制中间件
自定义内存级速率限制,针对不同路由实施差异化策略。
// 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/login | 15 分钟 | 5 次 | 防止暴力破解密码 |
/api/auth/* | 1 分钟 | 20 次 | 防止认证接口滥用 |
/api/copilot | 1 分钟 | 30 次 | 防止 LLM API 费用失控 |
/api/settings/api-keys | 1 分钟 | 10 次 | 防止密钥频繁操作 |
/api/webhooks | 1 秒 | 10 次 | 允许批量告警推送 |
| 其他路由 | 1 分钟 | 100 次 | 通用防护 |
20.7 SSH 命令过滤
命令过滤中间件基于正则表达式匹配危险命令,按用户角色实施不同级别的管控。
// 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 (放行)角色与权限矩阵:
| 操作类型 | viewer | operator | admin |
|---|---|---|---|
| 只读命令 | 允许 | 允许 | 允许 |
| 破坏性文件操作 | 拦截 | 拦截 | 允许 |
| 系统关键操作 | 拦截 | 拦截 | 拦截 |
| 凭据访问 | 告警 | 允许 | 允许 |
| 权限提升 | 告警 | 允许 | 允许 |
20.8 审计日志系统
所有关键操作均记录审计日志,支持追溯和安全合规。
-- 审计日志表结构
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);记录的审计事件:
| 事件 | action | resource_type | 记录内容 |
|---|---|---|---|
| 用户登录 | login | auth | 用户名、IP |
| 用户登出 | logout | auth | 用户名 |
| 修改密码 | change_password | auth | 用户名 |
| 创建服务器 | create_server | server | 服务器名称、IP |
| 执行命令 | execute_command | server | 命令内容、结果 |
| 创建 Agent | create_agent | agent | Agent 名称 |
| 执行任务 | execute_task | task | 任务 ID、工作流 |
| 密钥轮换 | rotate_key | security | 旧密钥 ID |
20.9 CORS 配置
跨域资源共享 (CORS) 通过环境变量灵活控制。
// 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
});// backend/src/utils/env.ts
ALLOWED_ORIGINS: getEnv('ALLOWED_ORIGINS', 'http://localhost:3000')
.split(',')
.map(s => s.trim())生产环境 CORS 配置示例:
# 开发环境(宽松)
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 的多层次安全防护体系:
- 认证层:JWT 双 Token 机制 + Token 黑名单实现安全的无状态认证
- 加密层:AES-256-GCM 加密敏感数据 + bcrypt 哈希密码
- 防护层:Helmet 安全头 + 速率限制 + 命令过滤
- 审计层:全操作日志记录,支持追溯和合规
- 网络层:CORS 白名单 + WebSocket 连接管控
这些安全机制相互配合,构成了纵深防御体系。理解并掌握这些机制,将帮助你在实际运维中确保平台的安全性。
本章练习
基础练习
实现 Token 过期自动刷新:在前端封装一个 HTTP 客户端(使用 Axios 或 Fetch),在 Access Token 过期时自动使用 Refresh Token 请求新 Token,并重放原始请求。
添加 IP 黑名单功能:在现有的
rateLimiter.ts基础上,扩展实现 IP 黑名单功能。当某个 IP 在 1 小时内触发 429 超过 10 次时,自动将其加入黑名单,拒绝所有请求。密码强度校验:在注册和修改密码时,增加密码强度校验:至少 8 位、包含大小写字母、数字和特殊字符。编写对应的正则表达式和中间件。
进阶练习
实现双因素认证 (2FA):使用 TOTP (Time-based One-Time Password) 算法实现双因素认证。用户首次登录时生成 QR 码,后续登录需要输入 6 位动态验证码。
实现 RBAC 权限模型:在现有的
requireRole基础上,设计基于角色的访问控制 (RBAC) 系统。定义角色-权限-资源的三层关系,支持动态配置。实现 API 签名验证:为 Webhook 接口实现 HMAC-SHA256 签名验证,参考项目中已有的
verifyWebhookSignature函数,确保外部推送的告警数据未被篡改。
思考题
项目使用内存中的
Map实现速率限制和 Token 黑名单。在分布式部署(多实例)场景下,这些机制会存在什么问题?如何基于 Redis 实现分布式限流和黑名单?AES-256-GCM 的密钥存储在 SQLite 数据库中。如果攻击者获得了数据库文件的访问权限,加密是否仍然有效?请分析"加密的密钥与数据同源"这一架构的安全风险,并提出改进方案。
延伸阅读
- OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- NIST 密码哈希指南: SP 800-63B
- JWT 安全最佳实践: RFC 8725
- Helmet 文档: https://helmetjs.github.io/
- 书籍推荐:《Web Security Deep Dive》- 系统学习 Web 安全
