关于 implementing-api-rate-limiting-and-throttling
这是面向 API 开发者和安全工程师的限流实施指南,涵盖算法选择、Redis 计数、分层配额和分布式部署。内容包括配置示例、常见陷阱及验证建议。
主要功能
- 01按端点与用户层级设计配额
- 02实现滑动窗口与令牌桶
- 03使用 Redis 维护分布式计数
- 04配置限流响应头与 429 响应
- 05监控限流指标并进行负载测试
适用场景
- 01保护登录和身份验证端点
- 02为不同订阅层级分配 API 配额
- 03限制自动化脚本造成的资源消耗
- 04在多实例服务中执行统一限流
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mukul975/ anthropic-cybersecurity-skills --skill implementing-api-rate-limiting-and-throttling安装后在 Claude Code 中输入/implementing-api-rate-limiting-and-throttling确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSEreferences/api-reference.mdscripts/agent.py详细介绍
实现 API 限流与节流
适用场景
- 保护身份验证端点,防范暴力破解和凭据填充攻击
- 防止自动化脚本和机器人滥用 API 并耗尽资源
- 为不同 API 使用方层级(免费、高级、企业)实施公平的使用配额
- 防御应用层拒绝服务攻击
- 满足要求实施 API 滥用防护措施的合规要求
不要仅将限流作为抵御攻击的唯一防线。应结合身份验证、授权和 WAF 规则。
前提条件
- Redis 6.0+,用于分布式限流计数器(单实例部署也可使用内存)
- API 框架(Express.js、FastAPI、Spring Boot 或 Django REST Framework)
- 用于监控限流指标的系统(Prometheus、CloudWatch、Datadog)
- 了解 API 的正常流量模式和峰值使用情况
- 用于验证限流行为的负载测试工具(k6、Gatling 或 Locust)
工作流
步骤 1:设计限流策略
按端点类别和用户层级定义限流值:
# Rate limit configuration
RATE_LIMITS = {
# Authentication endpoints (most restrictive)
"auth": {
"login": {"requests": 5, "window_seconds": 60, "by": "ip"},
"register": {"requests": 3, "window_seconds": 300, "by": "ip"},
"forgot_password": {"requests": 3, "window_seconds": 3600, "by": "ip"},
"verify_mfa": {"requests": 5, "window_seconds": 300, "by": "user"},
},
# Standard API endpoints
"api": {
"free": {"requests": 60, "window_seconds": 60, "by": "user"},
"premium": {"requests": 300, "window_seconds": 60, "by": "user"},
"enterprise": {"requests": 1000, "window_seconds": 60, "by": "user"},
},
# Resource-intensive endpoints
"expensive": {
"search": {"requests": 10, "window_seconds": 60, "by": "user"},
"export": {"requests": 5, "window_seconds": 3600, "by": "user"},
"bulk_import": {"requests": 2, "window_seconds": 3600, "by": "user"},
},
# Global limits
"global": {
"per_ip": {"requests": 1000, "window_seconds": 60, "by": "ip"},
"per_user": {"requests": 5000, "window_seconds": 3600, "by": "user"},
},
}
步骤 2:滑动窗口限流器(Redis)
import redis
import time
import hashlib
from functools import wraps
from flask import Flask, request, jsonify, g
app = Flask(__name__)
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
class SlidingWindowRateLimiter:
"""Sliding window rate limiter using Redis sorted sets."""
def __init__(self, redis_conn):
self.redis = redis_conn
def is_allowed(self, key, max_requests, window_seconds):
"""Check if request is allowed and record it."""
now = time.time()
window_start = now - window_seconds
pipe = self.redis.pipeline()
# Remove expired entries
pipe.zremrangebyscore(key, 0, window_start)
# Count requests in current window
pipe.zcard(key)
# Add current request
pipe.zadd(key, {f"{now}:{hashlib.md5(str(now).encode()).hexdigest()[:8]}": now})
# Set TTL on the key
pipe.expire(key, window_seconds + 1)
results = pipe.execute()
current_count = results[1]
if current_count >= max_requests:
# Calculate retry-after
oldest = self.redis.zrange(key, 0, 0, withscores=True)
if oldest:
retry_after = int(oldest[0][1] + window_seconds - now) + 1
else:
retry_after = window_seconds
return False, current_count, max_requests, retry_after
return True, current_count + 1, max_requests, 0
rate_limiter = SlidingWindowRateLimiter(redis_client)
def rate_limit(max_requests, window_seconds, key_func=None):
"""Decorator for rate limiting API endpoints."""
def decorator(f):
@wraps(f)
def wrapped(*args, **kwargs):
# Determine the rate limit key
if key_func:
identifier = key_func()
elif hasattr(g, 'user_id'):
identifier = f"user:{g.user_id}"
else:
identifier = f"ip:{request.remote_addr}"
key = f"ratelimit:{request.endpoint}:{identifier}"
allowed, current, limit, retry_after = rate_limiter.is_allowed(
key, max_requests, window_seconds)
# Always set rate limit headers
headers = {
"X-RateLimit-Limit": str(limit),
"X-RateLimit-Remaining": str(max(0, limit - current)),
"X-RateLimit-Reset": str(int(time.time()) + window_seconds),
}
if not allowed:
headers["Retry-After"] = str(retry_after)
response = jsonify({
"error": "rate_limit_exceeded",
"message": "Too many requests. Please try again later.",
"retry_after": retry_after
})
response.status_code = 429
for h, v in headers.items():
response.headers[h] = v
return response
response = f(*args, **kwargs)
for h, v in headers.items():
response.headers[h] = v
return response
return wrapped
return decorator
# Apply rate limiting to endpoints
@app.route('/api/v1/auth/login', methods=['POST'])
@rate_limit(max_requests=5, window_seconds=60,
key_func=lambda: f"ip:{request.remote_addr}")
def login():
# Login logic
return jsonify({"message": "Login successful"})
@app.route('/api/v1/users/me', methods=['GET'])
@rate_limit(max_requests=60, window_seconds=60)
def get_profile():
# Profile logic
return jsonify({"user": "data"})
@app.route('/api/v1/search', methods=['GET'])
@rate_limit(max_requests=10, window_seconds=60)
def search():
# Search logic
return jsonify({"results": []})
步骤 3:令牌桶限流器
import redis
import time
class TokenBucketRateLimiter:
"""Token bucket rate limiter allowing burst traffic within limits."""
def __init__(self, redis_conn):
self.redis = redis_conn
def is_allowed(self, key, max_tokens, refill_rate, refill_interval=1):
"""
Token bucket algorithm:
- max_tokens: Maximum burst capacity
- refill_rate: Tokens added per refill_interval
- refill_interval: Seconds between refills
"""
now = time.time()
bucket_key = f"tb:{key}"
# Lua script for atomic token bucket operation
lua_script = """
local key = KEYS[1]
local max_tokens = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local refill_interval = tonumber(ARGV[3])
local now = tonumber(ARGV[4])
local bucket = redis.call('hmget', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1])
local last_refill = tonumber(bucket[2])
if tokens == nil then
tokens = max_tokens
last_refill = now
end
-- Refill tokens
local elapsed = now - last_refill
local refills = math.floor(elapsed / refill_interval)
if refills > 0 then
tokens = math.min(max_tokens, tokens + (refills * refill_rate))
last_refill = last_refill + (refills * refill_interval)
end
local allowed = 0
if tokens >= 1 then
tokens = tokens - 1
allowed = 1
end
redis.call('hmset', key, 'tokens', tokens, 'last_refill', last_refill)
redis.call('expire', key, math.ceil(max_tokens / refill_rate * refill_interval) + 10)
return {allowed, tokens, max_tokens}
"""
result = self.redis.eval(lua_script, 1, bucket_key,
max_tokens, refill_rate, refill_interval, now)
allowed = bool(result[0])
remaining = int(result[1])
limit = int(result[2])
return allowed, remaining, limit
步骤 4:按用户方案分层限流
from enum import Enum
class UserTier(Enum):
FREE = "free"
PREMIUM = "premium"
ENTERPRISE = "enterprise"
TIER_LIMITS = {
UserTier.FREE: {
"default": (60, 60), # 60 req/min
"search": (10, 60), # 10 req/min
"export": (5, 3600), # 5 req/hour
"daily_total": (1000, 86400), # 1000 req/day
},
UserTier.PREMIUM: {
"default": (300, 60),
"search": (50, 60),
"export": (20, 3600),
"daily_total": (10000, 86400),
},
UserTier.ENTERPRISE: {
"default": (1000, 60),
"search": (200, 60),
"export": (100, 3600),
"daily_total": (100000, 86400),
},
}
def get_rate_limit_for_request(user_tier, endpoint_category="default"):
"""Get rate limit configuration based on user tier and endpoint."""
tier_config = TIER_LIMITS.get(user_tier, TIER_LIMITS[UserTier.FREE])
limit_config = tier_config.get(endpoint_category, tier_config["default"])
return limit_config # (max_requests, window_seconds)
class TieredRateLimitMiddleware:
"""Middleware that applies rate limits based on user subscription tier."""
def __init__(self, app, redis_conn):
self.app = app
self.limiter = SlidingWindowRateLimiter(redis_conn)
def __call__(self, environ, start_response):
# Extract user info from request
user_id = environ.get("HTTP_X_USER_ID")
user_tier = UserTier(environ.get("HTTP_X_USER_TIER", "free"))
endpoint = environ.get("PATH_INFO", "/")
# Determine endpoint category
category = "default"
if "/search" in endpoint:
category = "search"
elif "/export" in endpoint:
category = "export"
max_requests, window = get_rate_limit_for_request(user_tier, category)
key = f"tiered:{user_id or environ.get('REMOTE_ADDR')}:{category}"
allowed, current, limit, retry_after = self.limiter.is_allowed(
key, max_requests, window)
if not allowed:
status = "429 Too Many Requests"
headers = [
("Content-Type", "application/json"),
("Retry-After", str(retry_after)),
("X-RateLimit-Limit", str(limit)),
("X-RateLimit-Remaining", "0"),
]
start_response(status, headers)
body = f'{{"error":"rate_limit_exceeded","retry_after":{retry_after},"tier":"{user_tier.value}"}}'
return [body.encode()]
return self.app(environ, start_response)
步骤 5:微服务分布式限流
# Centralized rate limiting service using Redis Cluster
import redis
from redis.cluster import RedisCluster
class DistributedRateLimiter:
"""Rate limiter for microservice architectures using Redis Cluster."""
def __init__(self):
self.redis = RedisCluster(
startup_nodes=[
{"host": "redis-node-1", "port": 6379},
{"host": "redis-node-2", "port": 6379},
{"host": "redis-node-3", "port": 6379},
],
decode_responses=True
)
def check_and_increment(self, service_name, user_id, endpoint,
max_requests, window_seconds):
"""Atomic check-and-increment using Redis Lua script."""
key = f"rl:{{{service_name}}}:{user_id}:{endpoint}"
# Lua script ensures atomicity across the check and increment
lua_script = """
local key = KEYS[1]
local max_requests = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local window_start = now - window
-- Remove old entries
redis.call('zremrangebyscore', key, '-inf', window_start)
-- Count current entries
local count = redis.call('zcard', key)
if count >= max_requests then
-- Get oldest entry for retry-after calculation
local oldest = redis.call('zrange', key, 0, 0, 'WITHSCORES')
local retry_after = 0
if #oldest > 0 then
retry_after = math.ceil(tonumber(oldest[2]) + window - now)
end
return {0, count, retry_after}
end
-- Add new entry
redis.call('zadd', key, now, now .. ':' .. math.random(100000))
redis.call('expire', key, window + 1)
return {1, count + 1, 0}
"""
result = self.redis.eval(lua_script, 1, key,
max_requests, window_seconds, time.time())
return {
"allowed": bool(result[0]),
"current": int(result[1]),
"retry_after": int(result[2]),
}
核心概念
| 术语 | 定义 |
|---|---|
| 滑动窗口 | 在滚动时间窗口内跟踪请求的限流算法,相较于固定窗口可实现更平滑的限流 |
| 令牌桶 | 以固定速率添加令牌,并在每次请求时消耗令牌的算法;允许在桶容量范围内进行受控突发 |
| 固定窗口 | 最简单的限流方式,按固定时间窗口(例如,每分钟)统计请求数,但容易在窗口边界出现突发流量 |
| 429 Too Many Requests | 表示客户端超出限流值的 HTTP 状态码,通常附带 Retry-After 标头 |
| Retry-After 标头 | 告知客户端重试前应等待多少秒的 HTTP 响应标头,对于行为良好的 API 客户端至关重要 |
| 分布式限流 | 使用共享状态(Redis、Memcached)在多个服务实例间进行限流,以维护准确的全局计数器 |
工具与系统
- Redis:用于存储分布式限流计数器的内存数据存储,通过 Lua 脚本执行原子操作
- Kong Rate Limiting Plugin:API 网关插件,支持使用 Redis 后端实现固定窗口和滑动窗口限流
- express-rate-limit:Express.js 中间件,可通过 Redis、Memcached 或内存存储实现简单限流
- Flask-Limiter:Flask 扩展,支持多种后端,并可按端点配置限流值
- Envoy Rate Limit Service:面向基于 Envoy 的服务网格架构的集中式限流服务
常见场景
场景:为公共 API 实现速率限制
背景:一家公司推出了包含免费版、高级版和企业版的公共 API。API 必须防止滥用,同时为付费客户提供公平的访问权限。该 API 在 AWS ALB 后方运行于 6 个实例上。
方案:
- 部署 Redis Cluster(3 个节点),用于存储分布式速率限制状态
- 使用 Redis 有序集合和 Lua 脚本实现滑动窗口速率限制,确保原子性
- 配置各层级的限制:免费版(60 次请求/分钟)、高级版(300 次请求/分钟)、企业版(1000 次请求/分钟)
- 为身份验证端点设置更严格的限制(每个 IP 5 次请求/分钟),不受层级影响
- 为资源密集型端点设置限制(搜索:免费版 10 次请求/分钟;导出:5 次请求/小时)
- 在每个响应中设置速率限制响应标头(X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Reset)
- 超出限制时,返回包含 Retry-After 标头和 JSON 错误正文的 429 响应
- 设置 Prometheus 指标以统计速率限制触发次数,并配置 CloudWatch 警报以检测异常模式
注意事项:
- 在实例上使用内存中的速率限制而不共享状态,导致客户端可以通过访问不同服务器绕过限制
- 未将身份验证端点的速率限制与常规 API 限制分开实施
- 使用固定窗口,导致窗口边界处出现突发流量(短时间内达到 2 倍限制)
- 未在成功响应中包含速率限制标头,导致客户端无法了解自己的配额
- 未验证 X-Forwarded-For 是否来自负载均衡器,就信任它来识别 IP
输出格式
## Rate Limiting Implementation Report
**API**: Public API v2
**Algorithm**: Sliding Window (Redis Sorted Sets)
**Backend**: Redis Cluster (3 nodes)
**Deployment**: 6 API instances behind AWS ALB
### Rate Limit Configuration
| Tier | Default | Search | Export | Auth (per IP) |
|------|---------|--------|--------|---------------|
| Free | 60/min | 10/min | 5/hour | 5/min |
| Premium | 300/min | 50/min | 20/hour | 5/min |
| Enterprise | 1000/min | 200/min | 100/hour | 10/min |
### Validation Results (k6 load test)
- Free tier: Rate limited at 61st request (correct)
- Premium tier: Rate limited at 301st request (correct)
- Cross-instance: Rate limiting consistent across all 6 instances
- Redis failover: Rate limiting degrades gracefully (allows traffic) when Redis is unreachable
- Retry-After header: Accurate within 1 second of actual reset time
- Response overhead: < 2ms added latency per request for rate limit check
评论
支持 Markdown