会话累积成本速率限制器¶
概述¶
速率限制器通过在时间窗口内跟踪每个会话的累积请求成本,保护您的 RAG 聊天机器人免受低频持续攻击。
主要特性¶
不是传统的速率限制¶
❌ 传统方式: "每分钟 10 个请求"
✅ 本系统: "每小时 20 个请求,然后 15 分钟冷却"
多维度识别¶
该系统使用以下方式追踪请求:
- 会话 ID(来自
x-session-id标头或sessionId查询参数) - IP 地址(来自 Cloudflare 的
cf-connecting-ip标头) - 浏览器指纹(来自
x-fingerprint标头)
至少需要一个标识符。多个标识符可提供更强的保护。
累积成本模型¶
if (session.requestsInLastHour > 20) {
return "太多请求。请在 15 分钟后重试。";
}
对于普通用户: 几乎透明(默认:20 请求/小时)
对于攻击脚本: 完全无效
架构¶
┌─────────────────┐
│ 客户端请求 │
└────────┬────────┘
│
v
┌─────────────────────────────────┐
│ 提取会话标识符 │
│ - 会话 ID │
│ - IP 地址 │
│ - 指纹 │
└────────┬────────────────────────┘
│
v
┌─────────────────────────────────┐
│ 检查 KV 中的现有记录 │
│ 键:ratelimit:sid:xxx:ip:xxx │
└────────┬────────────────────────┘
│
v
┌────┴────┐
│ 找到? │
└────┬────┘
│
┌────┴─────────────────────┐
│ │
v v
┌───────────┐ ┌──────────────┐
│ 无记录 │ │ 检查窗口 │
│ │ │ 和计数 │
└─────┬─────┘ └──────┬───────┘
│ │
v ┌────┴────┐
┌──────────┐ │ 有效? │
│ 计数 = 1 │ └────┬────┘
│ 允许 │ │
└──────────┘ ┌───────────┴─────────────┐
│ │
v v
┌────────────┐ ┌────────────────┐
│ < 最大请求?│ │ 冷却中? │
└─────┬──────┘ └────┬───────────┘
│ │
┌───┴────┐ ┌───┴────┐
v v v v
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│允许 │ │阻止 │ │阻止 │ │重置 │
│计数 + │ │冷却 │ │429 │ │允许 │
└──────┘ └──────┘ └──────┘ └──────┘
实现细节¶
- 速率限制中间件实现位于
tools/rag-worker/src/middleware/rate-limiter.ts,Worker 核心逻辑在tools/rag-worker/src/worker.ts中接入此中间件。 - KV 绑定名(示例):
RATE_LIMIT_KV,请在wrangler.toml中将[[kv_namespaces]]的binding与实际创建的 namespace ID 对应起来。 - 单元测试位于
tests/unit/rate-limiter.test.ts,可运行npm test tests/unit/rate-limiter.test.ts来验证行为。
配置¶
默认设置¶
{
enabled: true,
timeWindowMs: 60 * 60 * 1000, // 1 小时
maxRequests: 20, // 20 个请求
cooldownMs: 15 * 60 * 1000 // 15 分钟
}
环境变量¶
在 wrangler.toml 中配置:
[vars]
RATE_LIMIT_ENABLED = "true" # 启用/禁用
RATE_LIMIT_WINDOW_MS = "3600000" # 时间窗口(毫秒)
RATE_LIMIT_MAX_REQUESTS = "20" # 窗口内最大请求数
RATE_LIMIT_COOLDOWN_MS = "900000" # 冷却持续时间(毫秒)
使用¶
设置 KV 命名空间¶
# 创建 KV 命名空间
wrangler kv:namespace create "RATE_LIMIT_KV"
# 添加到 wrangler.toml
[[kv_namespaces]]
binding = "RATE_LIMIT_KV"
id = "your-namespace-id"
客户端集成¶
基本使用(会话 ID)¶
// 生成或检索会话 ID
function getSessionId() {
let sessionId = localStorage.getItem('chat-session-id');
if (!sessionId) {
sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
localStorage.setItem('chat-session-id', sessionId);
}
return sessionId;
}
// 发送带有会话 ID 的请求
async function sendMessage(message) {
const response = await fetch('/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-session-id': getSessionId()
},
body: JSON.stringify({ message })
});
if (response.status === 429) {
const error = await response.json();
alert(`太多请求。请在 ${error.retryAfter} 秒后重试。`);
return;
}
return response.json();
}
增强保护(指纹识别)¶
import FingerprintJS from '@fingerprintjs/fingerprintjs';
async function getFingerprint() {
const fp = await FingerprintJS.load();
const result = await fp.get();
return result.visitorId;
}
async function sendMessage(message) {
const fingerprint = await getFingerprint();
const response = await fetch('/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-session-id': getSessionId(),
'x-fingerprint': fingerprint
},
body: JSON.stringify({ message })
});
// 处理响应...
}
响应格式¶
成功 (200)¶
{
"answer": "响应文本",
"sources": [...]
}
速率限制 (429)¶
{
"error": "太多请求",
"message": "您在短时间内发送了太多请求。",
"cooldown": "冷却:900 秒",
"retryAfter": 900,
"tip": "正常使用不会触发此限制。",
"details": {
"requestCount": 21,
"windowStart": "2025-12-16T10:00:00.000Z",
"lastRequest": "2025-12-16T10:10:00.000Z",
"coolingUntil": "2025-12-16T10:15:00.000Z"
}
}
响应标头:
Retry-After: 允许重试前的秒数X-RateLimit-Limit: 触发限制的请求计数X-RateLimit-Remaining: 0(超过限制)X-RateLimit-Reset: 限制重置的 Unix 时间戳
监控¶
Cloudflare 日志¶
搜索:
operation: rate_limit_exceeded
operation: rate_limit_passed
日志条目示例:
{
"operation": "rate_limit_exceeded",
"metadata": {
"identifier": "session:test-session-123",
"requestCount": 21,
"coolingUntil": "2025-12-16T10:25:00.000Z",
"ttl": 900
}
}
管理端点(可选)¶
添加到 worker.ts 用于调试:
if (req.method === 'GET' && url.pathname === '/admin/rate-limit-status') {
const auth = req.headers.get('authorization');
if (auth !== `Bearer ${env.ADMIN_TOKEN}`) {
return new Response('未授权', { status: 401 });
}
const sessionId = url.searchParams.get('sessionId');
if (!sessionId) {
return new Response('需要 sessionId', { status: 400 });
}
const key = `ratelimit:sid:${sessionId}`;
const record = await env.RATE_LIMIT_KV.get(key, 'json');
return new Response(JSON.stringify(record, null, 2), {
headers: { 'Content-Type': 'application/json' }
});
}
场景和调优¶
场景 1:个人博客(默认)¶
低流量,宽松限制:
RATE_LIMIT_WINDOW_MS = "3600000" # 1 小时
RATE_LIMIT_MAX_REQUESTS = "20" # 20 个请求
RATE_LIMIT_COOLDOWN_MS = "900000" # 15 分钟
场景 2:高流量文档¶
对重度用户更宽松:
RATE_LIMIT_WINDOW_MS = "3600000" # 1 小时
RATE_LIMIT_MAX_REQUESTS = "50" # 50 个请求
RATE_LIMIT_COOLDOWN_MS = "600000" # 10 分钟
场景 3:严格保护¶
激进的安全限制:
RATE_LIMIT_WINDOW_MS = "1800000" # 30 分钟
RATE_LIMIT_MAX_REQUESTS = "10" # 10 个请求
RATE_LIMIT_COOLDOWN_MS = "1800000" # 30 分钟
场景 4:临时禁用¶
紧急覆盖:
RATE_LIMIT_ENABLED = "false"
故障排除¶
问题:速率限制未强制执行¶
检查清单:
- 验证
RATE_LIMIT_ENABLED不是"false" - 确认 KV 命名空间绑定正确
- 检查客户端是否发送会话 ID 或具有有效 IP
- 查看 Worker 日志中的
rate_limit_*事件
问题:误报(合法用户被阻止)¶
解决方案:
- 增加
RATE_LIMIT_MAX_REQUESTS(例如 20 → 50) - 扩展
RATE_LIMIT_WINDOW_MS(例如 1h → 2h) - 减少
RATE_LIMIT_COOLDOWN_MS(例如 15min → 5min) - 将特定 IP 或会话 ID 加入白名单(需要自定义逻辑)
问题:攻击者绕过限制¶
可能原因:
- 攻击者使用代理池(旋转 IP)
- 没有会话 ID 强制
增强功能:
- 在客户端代码中强制要求会话 ID
- 添加浏览器指纹识别(
x-fingerprint) - 对新/未识别的会话收紧限制
- 考虑 Cloudflare Bot Management(付费)
成本分析¶
KV 操作¶
- 免费层: 每天 100,000 次读取,1,000 次写入
- 付费: \(0.50/百万次读取,\)5.00/百万次写入
示例(每天 10,000 次聊天):
- 每个请求 2 个 KV 操作(1 次读取 + 1 次写入)
- 20,000 操作/天 = 600,000 操作/月
- 成本: $0(在免费层内)
Worker CPU¶
速率限制开销:每个请求约 1-2 毫秒(可忽略不计)
测试¶
运行包含的测试套件:
npm test tests/unit/rate-limiter.test.ts
手动测试脚本:
#!/bin/bash
SESSION_ID="test-$(date +%s)"
for i in {1..25}; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST https://your-worker.dev/chat \
-H "Content-Type: application/json" \
-H "x-session-id: $SESSION_ID" \
-d '{"message":"test"}')
echo "请求 $i: $STATUS"
[ "$STATUS" = "429" ] && echo "✅ 速率限制已触发" && break
sleep 0.5
done
最佳实践¶
- 始终从客户端发送会话 ID 以准确追踪
- 定期监控日志 以根据实际使用情况调优限制
- 从保守开始(默认配置),如果需要可以放松
- 添加指纹识别 以增强对复杂攻击的保护
- 在客户端实现重试逻辑 使用指数退避
- 清楚地传达限制 在用户面向的错误消息中
总结¶
✅ 有效防御: 阻止低频持续攻击 ✅ 用户友好: 对正常使用透明 ✅ 灵活配置: 针对您的流量模式进行调整 ✅ 具有成本效益: 在 Cloudflare 免费层内运行 ✅ 可观测性: 全面的日志记录和监控
从默认设置开始,根据您的具体需求和攻击模式进行调整。