API 中的 Rate Limiting:防止滥用的保护机制
Rate limiting 是保护 API 免受滥用、过度资源消耗和拒绝服务攻击的一项基本技术。适当的实现可确保合法用户的可用性,同时阻止恶意或过度的行为。
为什么要实施 Rate Limiting?
- 防止 DDoS:缓解拒绝服务攻击
- 防止 Scraping:增加自动化数据提取的难度
- 控制成本:避免过度消耗计算资源
- 保证服务质量:公平分配资源
- 防止 Brute Force:限制身份验证尝试次数
Rate Limiting 算法
1. Token Bucket
一种维护随时间补充的令牌"桶"的算法:
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity; // Capacidade máxima do balde
this.tokens = capacity; // Tokens disponíveis
this.refillRate = refillRate; // Tokens por segundo
this.lastRefill = Date.now();
}
tryConsume(tokens = 1) {
this.refill();
if (this.tokens >= tokens) {
this.tokens -= tokens;
return true; // Requisição permitida
}
return false; // Rate limit excedido
}
refill() {
const now = Date.now();
const timePassed = (now - this.lastRefill) / 1000;
const tokensToAdd = timePassed * this.refillRate;
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}
// Uso: 100 requisições máximo, recarrega 10/segundo
const bucket = new TokenBucket(100, 10);
优点:允许受控的突发流量,平滑流量
缺点:实现更为复杂
2. Leaky Bucket
以恒定速率处理请求,就像水从桶中漏出一样:
- 请求进入桶中
- 以固定速率处理
- 多余的部分溢出(被拒绝)
- 保证均匀的输出
3. Fixed Window
在固定时间窗口内对请求进行计数:
class FixedWindowRateLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = new Map();
}
isAllowed(userId) {
const now = Date.now();
const windowStart = Math.floor(now / this.windowMs) * this.windowMs;
const key = \`\$:\$\`;
const count = this.requests.get(key) || 0;
if (count < this.maxRequests) {
this.requests.set(key, count + 1);
return true;
}
return false;
}
}
// 100 requisições por hora
const limiter = new FixedWindowRateLimiter(100, 60 * 60 * 1000);
问题:在窗口边缘允许达到限制的 2 倍
4. Sliding Window Log
维护请求时间戳的日志:
- 存储每个请求的时间戳
- 移除窗口外的请求
- 比 Fixed Window 更精确
- 内存消耗更高
5. Sliding Window Counter
将 Fixed Window 与平滑处理相结合:
// Calcula uma média ponderada entre janelas atual e anterior
const currentWindowCount = getCurrentWindowCount(userId);
const previousWindowCount = getPreviousWindowCount(userId);
const percentageInCurrentWindow = (now - currentWindowStart) / windowSize;
const estimatedCount =
previousWindowCount * (1 - percentageInCurrentWindow) +
currentWindowCount;
return estimatedCount < maxRequests;
实践实现
使用 Redis(推荐用于生产环境)
import Redis from 'ioredis';
const redis = new Redis();
async function checkRateLimit(userId, maxRequests = 100, windowSeconds = 60) {
const key = \`rate_limit:\$\`;
const now = Date.now();
const windowStart = now - (windowSeconds * 1000);
// Remover requisições antigas
await redis.zremrangebyscore(key, 0, windowStart);
// Contar requisições na janela
const requestCount = await redis.zcard(key);
if (requestCount < maxRequests) {
// Adicionar nova requisição
await redis.zadd(key, now, \`\$-\${Math.random()}\`);
await redis.expire(key, windowSeconds);
return { allowed: true, remaining: maxRequests - requestCount - 1 };
}
return { allowed: false, remaining: 0 };
}
// Middleware Express
app.use(async (req, res, next) => {
const userId = req.user?.id || req.ip;
const result = await checkRateLimit(userId);
res.set({
'X-RateLimit-Limit': 100,
'X-RateLimit-Remaining': result.remaining,
'X-RateLimit-Reset': new Date(Date.now() + 60000).toISOString()
});
if (!result.allowed) {
return res.status(429).json({
error: 'Too Many Requests',
retryAfter: 60
});
}
next();
});
常用库
- express-rate-limit:用于 Express.js 的 Middleware
- rate-limiter-flexible:支持多种 backends(Redis、Memcached、MySQL)
- Kong Rate Limiting:用于 API Gateway 的 Plugin
- AWS API Gateway:原生 rate limiting
高级策略
分层 Rate Limiting
- 全局:API 的总限制(例如:1M req/min)
- 按用户:个人限制(例如:1000 req/min)
- 按 Endpoint:特定限制(login:5 req/min)
- 按 IP:针对滥用的额外保护
动态 Rate Limiting
- 根据系统负载调整限制
- 为高级用户提高限制
- 在事件期间降低限制
Whitelisting 和 Blacklisting
- 豁免受信任的 IP/用户
- 永久阻止已知攻击者
- 实施信誉系统
最佳实践
- 返回信息性标头(X-RateLimit-*)
- 使用 HTTP 状态码 429(Too Many Requests)
- 包含 Retry-After 标头
- 在 API 中清晰地记录限制
- 在客户端实现指数退避
- 监控 rate limiting 指标
- 对异常模式发出警报
- 在投入生产前测试限制
监控工具
- Grafana + Prometheus:可视化 rate limiting 指标
- Datadog:监控和告警
- CloudWatch:用于 AWS 上的 API
- New Relic:支持 rate limiting 的 APM
