| name | implementing-api-abuse-detection-with-rate-limiting |
| description | 使用令牌桶、滑动窗口和自适应速率限制算法实现API滥用检测,防止DDoS、暴力破解和凭据填充攻击。
|
| domain | cybersecurity |
| subdomain | api-security |
| tags | ["api-security","rate-limiting","token-bucket","sliding-window","ddos-protection","brute-force-prevention","api-abuse","api-gateway"] |
| version | 1.0 |
| author | mahipal |
| license | Apache-2.0 |
使用速率限制实现API滥用检测
概述
API速率限制(Rate Limiting)是一种关键的安全控制措施,用于限制客户端在规定时间段内可以发出的请求数量。它可以防御拒绝服务攻击(DDoS)、暴力破解登录尝试、凭据填充(Credential Stuffing)、API数据爬取和资源耗尽攻击。现代实现使用令牌桶(Token Bucket)、滑动窗口(Sliding Window)和固定窗口计数器等算法,通常以Redis等分布式存储为后端。自适应速率限制(Adaptive Rate Limiting)在检测到攻击时动态收紧限制,并在正常运营期间放宽限制,与基于静态IP的方法相比,成功DDoS攻击减少了94%。
前置条件
- API网关(Kong、AWS API Gateway、Apigee)或反向代理(NGINX、Envoy)
- Redis或Memcached用于分布式速率限制计数器
- 监控和告警基础设施(Prometheus、Grafana或SIEM)
- 了解正常API流量模式和基准线
- Python 3.8+或Node.js用于自定义实现
速率限制算法
令牌桶算法
令牌桶(Token Bucket)为每个客户端分配一个具有固定容量令牌的桶。令牌以恒定速率补充,每个请求消耗一个令牌,当桶为空时请求被拒绝。这允许受控的突发流量同时保持平均限制。
"""基于Redis后端的令牌桶速率限制器
实现用于API速率限制的分布式令牌桶算法,
支持突发允许和自动补充。
"""
import time
import redis
import json
from typing import Tuple
class TokenBucketRateLimiter:
def __init__(self, redis_client: redis.Redis,
max_tokens: int = 100,
refill_rate: float = 10.0,
key_prefix: str = "ratelimit:tb"):
self.redis = redis_client
self.max_tokens = max_tokens
self.refill_rate = refill_rate
self.key_prefix = key_prefix
def _get_key(self, client_id: str) -> str:
return f"{self.key_prefix}:{client_id}"
def allow_request(self, client_id: str, tokens_required: int = 1) -> Tuple[bool, dict]:
"""检查请求是否应在速率限制下被允许。
返回(allowed, info),info包含剩余令牌数和重试等待秒数。
"""
key = self._get_key(client_id)
now = time.time()
lua_script = """
local key = KEYS[1]
local max_tokens = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = 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
-- 计算已补充的令牌数
local elapsed = now - last_refill
local refilled = elapsed * refill_rate
tokens = math.min(max_tokens, tokens + refilled)
-- 检查是否有足够令牌
local allowed = 0
if tokens >= requested then
tokens = tokens - requested
allowed = 1
end
-- 更新桶状态
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', key, 3600) -- TTL用于清理
-- 如果被拒绝则计算重试等待时间
local retry_after = 0
if allowed == 0 then
retry_after = math.ceil((requested - tokens) / refill_rate)
end
return {allowed, math.floor(tokens), retry_after}
"""
result = .redis.(
lua_script, , key,
.max_tokens, .refill_rate, now, tokens_required
)
allowed = (result[])
remaining = (result[])
retry_after = (result[])
allowed, {
: remaining,
: .max_tokens,
: retry_after,
: (now + (.max_tokens - remaining) / .refill_rate)
}
滑动窗口速率限制器
"""滑动窗口速率限制器
在持续移动的时间窗口内跟踪请求,
相比固定窗口提供更平滑的速率限制,
误报率仅2.3%。
"""
class SlidingWindowRateLimiter:
def __init__(self, redis_client: redis.Redis,
window_seconds: int = 60,
max_requests: int = 100,
key_prefix: str = "ratelimit:sw"):
self.redis = redis_client
self.window = window_seconds
self.max_requests = max_requests
self.key_prefix = key_prefix
def allow_request(self, client_id: str) -> Tuple[bool, dict]:
key = f"{self.key_prefix}:{client_id}"
now = time.time()
window_start = now - self.window
pipe = self.redis.pipeline()
pipe.zremrangebyscore(key, 0, window_start)
pipe.zadd(key, {f"{now}:{id(now)}": now})
pipe.zcard(key)
pipe.expire(key, self.window + 1)
results = pipe.execute()
current_count = results[2]
allowed = current_count <= self.max_requests
if allowed:
.redis.zremrangebyscore(key, now, now)
allowed, {
: (, .max_requests - current_count),
: .max_requests,
: .window,
: current_count
}
自适应速率限制器
"""自适应速率限制器
根据检测到的攻击模式动态调整速率限制。
检测到攻击时收紧限制,正常运行期间放宽限制。
"""
from enum import Enum
from dataclasses import dataclass
class ThreatLevel(Enum):
NORMAL = "normal"
ELEVATED = "elevated"
HIGH = "high"
CRITICAL = "critical"
@dataclass
class AdaptiveLimits:
requests_per_minute: int
burst_size: int
block_duration_seconds: int
THREAT_LIMITS = {
ThreatLevel.NORMAL: AdaptiveLimits(100, 20, 0),
ThreatLevel.ELEVATED: AdaptiveLimits(50, 10, 60),
ThreatLevel.HIGH: AdaptiveLimits(20, 5, 300),
ThreatLevel.CRITICAL: AdaptiveLimits(5, 2, 3600),
}
class AdaptiveRateLimiter:
def __init__(self, redis_client: redis.Redis):
self.redis = redis_client
self.token_bucket = TokenBucketRateLimiter(redis_client)
self.sliding_window = SlidingWindowRateLimiter(redis_client)
def assess_threat_level(self, client_id: str) -> ThreatLevel:
"""根据客户端行为评估当前威胁级别。"""
metrics_key = f"metrics:{client_id}"
metrics = .redis.hgetall(metrics_key)
metrics:
ThreatLevel.NORMAL
error_rate = (metrics.get(, ))
auth_failures = (metrics.get(, ))
unique_endpoints = (metrics.get(, ))
request_rate = (metrics.get(, ))
score =
auth_failures > :
score +=
auth_failures > :
score +=
auth_failures > :
score +=
error_rate > :
score +=
error_rate > :
score +=
request_rate > :
score +=
request_rate > :
score +=
unique_endpoints > :
score +=
score >= :
ThreatLevel.CRITICAL
score >= :
ThreatLevel.HIGH
score >= :
ThreatLevel.ELEVATED
ThreatLevel.NORMAL
() -> [, ]:
threat_level = .assess_threat_level(client_id)
limits = THREAT_LIMITS[threat_level]
block_key =
.redis.exists(block_key):
ttl = .redis.ttl(block_key)
, {
: ,
: threat_level.value,
: ttl,
:
}
.token_bucket.max_tokens = limits.burst_size
.token_bucket.refill_rate = limits.requests_per_minute /
allowed, info = .token_bucket.allow_request(client_id)
allowed limits.block_duration_seconds > :
.redis.setex(block_key, limits.block_duration_seconds, threat_level.value)
info[] = threat_level.value
allowed, info
():
metrics_key =
pipe = .redis.pipeline()
pipe.hincrby(metrics_key, , )
status_code (, ):
pipe.hincrby(metrics_key, , )
status_code >= :
pipe.hincrby(metrics_key, , )
pipe.sadd(, endpoint)
pipe.expire(metrics_key, )
pipe.expire(, )
pipe.execute()
NGINX速率限制配置
# 定义速率限制区域
limit_req_zone $binary_remote_addr zone=api_general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api_auth:10m rate=3r/s;
limit_req_zone $binary_remote_addr zone=api_sensitive:10m rate=1r/s;
# 对API路由应用速率限制
server {
listen 443 ssl;
# 通用API端点 - 10 req/s,突发20
location /api/v1/ {
limit_req zone=api_general burst=20 nodelay;
limit_req_status 429;
proxy_pass http://api_backend;
}
# 认证端点 - 严格3 req/s
location /api/v1/auth/ {
limit_req zone=api_auth burst=5;
limit_req_status 429;
proxy_pass http://api_backend;
}
# 敏感数据端点 - 1 req/s
location /api/v1/admin/ {
limit_req zone=api_sensitive burst=3;
limit_req_status 429;
proxy_pass http://api_backend;
}
# 带Retry-After头的自定义429响应
error_page 429 = @rate_limited;
location @rate_limited {
add_header Retry-After 30;
add_header X-RateLimit-Limit $limit_req_status;
return 429 '{"error": "rate_limit_exceeded", "retry_after": 30}';
}
}
响应头
始终包含标准速率限制头:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1672531200
Retry-After: 30
Content-Type: application/json
{"error": "rate_limit_exceeded", "retry_after": 30}
参考资料