限流实战:如何保护登录端点不被压垮
周二凌晨 3 点,你的运维群炸了。CPU 飙到 95%,登录端点的 p99 延迟从 50ms 涨到 12 秒。日志显示 /auth/login 每秒收到 3000 次请求,来源是分布在全球 200 多个 IP 上的僵尸网络。攻击者正拿着泄露的密码库暴力破解你的登录。
你没有配置任何限流。你的登录端点完全暴露在外。
限流不是可选项
登录端点很特殊,必须加以保护:
- CPU 密集:密码校验需要执行 bcrypt/argon2 运算,开销远高于普通 API。一次 bcrypt 校验大约消耗 50-100ms 的 CPU。每秒三千次并发请求,意味着 150-300 个 CPU 核心的持续消耗。
- 有状态写入:登录失败会更新
failed_attempts计数器、写审计日志、触发失败次数检查。这些数据库写操作在高并发下会成为瓶颈。 - 安全风险:没有限流,攻击者可以在几分钟内尝试数万种密码组合。再强的密码,在足够多的尝试次数面前也终会失守。
限流算法的演进
第一代:固定窗口计数器
最简单的做法:在固定时间窗口(例如 1 分钟)内统计请求数,超过阈值的请求直接拒绝。
Logic:
key = "ratelimit:login:ip:{client_ip}"
count = redis.incr(key)
if count == 1: redis.expire(key, 60) # 60-second window
if count > 100: return 429 Too Many Requests
问题:边界突发
固定窗口有一个严重缺陷——窗口边界处的突发流量不受限制。
Timeline: |──── Minute 1 ────|──── Minute 2 ────|
Requests: 100 100
But if attackers concentrate requests in the last second of minute 1 and the first second of minute 2:
Timeline: |────Minute 1─────────|──Minute 2──|
Requests: 98 (59s) 100 (1s) 100 (1s)
In 2 seconds, attackers can send 200 requests, while your rate limit intends 100 per minute.
第二代:滑动窗口
滑动窗口把时间窗口切分成更小的槽位,从而解决边界突发问题。
Timeline (1-min window, 6 slots, 10s each):
┌──────┬──────┬──────┬──────┬──────┬──────┐
│ 0-10s│10-20s│20-30s│30-40s│40-50s│50-60s│
│ 15 │ 20 │ 18 │ 12 │ 8 │ 5 │
└──────┴──────┴──────┴──────┴──────┴──────┘
Current total = 15+20+18+12+8+5 = 78 < 100 → Pass
Next 10 seconds, window advances:
┌──────┬──────┬──────┬──────┬──────┬──────┐
│10-20s│20-30s│30-40s│40-50s│50-60s│60-70s│
│ 20 │ 18 │ 12 │ 8 │ 5 │ 0 │
└──────┴──────┴──────┴──────┴──────┴──────┘
Current total = 20+18+12+8+5+0 = 63 < 100 → Pass
滑动窗口比固定窗口精确得多,但在高精度场景下,粒度决定精度,存储成本也随之上升。
第三代:令牌桶
令牌桶是业界最流行的限流算法,也是 Autional gateway-service 的默认选择。
flowchart TD
R["补充器:每秒补充 r 个令牌"] --> B["令牌桶:最多容纳 b 个"]
B --> P["取到令牌,放行"]
B --> X["桶空,拒绝"]
图 1:令牌桶——补充器以固定速率 r 匀速发令牌,容量 b 决定能攒出多大的突发;取到令牌放行,桶空就拒绝。
核心参数:
- 速率 r:每秒补充的令牌数(稳态速率)
- 容量 b:桶中最多可容纳的令牌数(允许的突发量)
这正是令牌桶的精妙之处——可控的突发。当 r=10, b=100 时:正常情况下每秒放行 10 个请求;但如果桶在空闲期攒满了 100 个令牌,就能瞬间承接 100 个请求。这不违背长期平均速率——它只是把之前攒下的额度一次花掉。
第四代:漏桶
漏桶是令牌桶的镜像:令牌桶以固定速率补充令牌并允许突发;漏桶则以固定速率处理请求,把输出削峰填谷。
flowchart TD
Q["请求以任意速率涌入"] --> L["漏桶队列:容量固定"]
L --> O["固定速率流出,削峰填谷"]
Q --> X["队列已满,直接丢弃"]
图 2:漏桶——入队端来多快都接(直到装满),出队端永远匀速;队列满了就丢,用丢弃换来下游看到的平稳流量。
漏桶适合流量整形的场景——需要向下游服务输出稳定请求速率时。但面对突发流量,漏桶会直接丢弃而非排队,用户体验不如令牌桶。
Autional gateway-service 默认使用令牌桶,并提供配置项,允许租户管理员根据流量特征切换算法。
多维度限流:不止于 IP
基于 IP 的限流是最常见的做法,但它有两个局限:
- NAT/代理后的用户共用同一 IP:同一家公司 200 人通过一个出口 IP 访问你的服务——IP 级限流会误伤正常用户。
- 攻击者使用 IP 池:手握大量 IP 的攻击者可以对单个账号发起低频、有组织的攻击,每个 IP 都远低于阈值。
成熟的限流策略需要多层配合:
第一层:IP 级限流
IP-level parameters (Autional defaults):
- Window: 60 seconds
- Threshold: 30 requests / window
- Algorithm: sliding window
这是抵御大规模分布式攻击的最外层防线。当单个 IP 的请求量异常时,直接拒绝。
第二层:用户级限流
User-level parameters:
- Window: 5 minutes
- Threshold: 10 requests / window
- Algorithm: token bucket (r=0.03/s, b=10)
这是核心防御层。即使攻击者用不同 IP 针对同一账号,该账号每 5 分钟也最多只能尝试 10 次。这对阻断定向暴力破解至关重要。
第三层:全局限流
Global parameters:
- Window: 10 seconds
- Threshold: 500 requests / window (entire login endpoint)
这是灾难保护层。当整体登录请求量远超正常水平(说明正在遭受 DDoS 攻击)时,优先保障其他业务端点的可用性。
Autional gateway-service 三层配置示例
# Tenant admin configuration in Autional admin console
rate_limiting:
login_endpoint:
ip_limit:
window: 60s
max_requests: 30
algorithm: sliding_window
user_limit:
window: 300s
max_requests: 10
algorithm: token_bucket
global_limit:
window: 10s
max_requests: 500
algorithm: token_bucket
block_duration: 900s # 15-min block after rate limit triggered
block_strategy: progressive # 1st: 1min, 2nd: 5min, 3rd: 30min
分布式限流:多网关实例
在微服务架构下,单实例限流是不够的——3 个网关实例各自有 30 次/分钟的 IP 阈值,攻击者向每个实例各发 30 次,合计 90 次/分钟,轻松绕过限制。
分布式限流依赖共享的计数存储,Redis 是天然之选:
Distributed rate limiting with Redis:
# IP-level rate limiting (sliding window)
EVAL "
local key = KEYS[1]
local window = tonumber(ARGV[1]) -- window size in seconds
local limit = tonumber(ARGV[2]) -- threshold
local now = tonumber(ARGV[3]) -- current timestamp (ms)
local window_start = now - window * 1000
-- Remove entries outside the window
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
-- Count requests within the window
local count = redis.call('ZCARD', key)
if count >= limit then
return 0 -- reject
end
-- Add current request to sorted set
redis.call('ZADD', key, now, now .. ':' .. math.random())
redis.call('EXPIRE', key, window + 1)
return 1 -- pass
" 1 "ratelimit:login:ip:192.168.1.1" 60 30 1715692800000
Autional gateway-service 内置了这套 Redis 限流器,开发者无需自己实现。只要在网关配置中提供 redis 连接信息,就会自动启用分布式模式。若 Redis 不可用,则优雅降级为本地限流(各实例独立计数),并触发告警。
真实场景:一次完整的暴力破解防御链
回到开头的攻击场景。以下是 Autional 的逐层响应:
Time: 03:00:00
Attack begins → 3,000 login requests/second from 200+ IPs
03:00:02
Global rate limit triggered: requests exceed 500 in 10-second window
→ gateway-service returns 429 Too Many Requests
→ System auto-scales gateway-service instances (Kubernetes HPA)
03:00:05
IP-level rate limit triggered: each attacking IP is individually limited
→ Attacker IPs enter the blocklist for 15 minutes
→ Legitimate users are unaffected (their IPs are far below the threshold)
03:00:10
User-level rate limit triggered: multiple IPs detected trying the same account
→ Account enters "protected" mode
→ Subsequent login attempts require MFA (WebAuthn)
→ Security alert triggered, email sent to account owner
03:00:30
Adaptive MFA engine activates:
→ Composite score: unknown device fingerprint + low IP reputation + multi-location + high failure rate = extreme risk
→ Further requests for protected accounts are directly rejected
→ Security team receives alert push notification
03:05:00
Attack traffic subsides.
→ Blocked IPs auto-unblock after 15 minutes
→ System returns to normal
→ Audit log has a complete record of the entire attack
限流配置的黄金法则
1. 不要只依赖 IP 限流
IP 限流只是第一道防线,不是唯一一道。它必须与用户级限流配合使用。
2. 阈值应来自数据
不要凭感觉猜阈值,而要分析你的正常流量特征:
- 正常用户 1 分钟内会发起多少次登录尝试?(用 p99,不要用平均值)
- 正常用户每小时登录多少次?(取最大值)
- 你的登录端点请求速率的 p95 与 p99 分别是多少?
把阈值设在正常 p99 的 3-5 倍——既能容忍异常行为,又能有效拦截攻击。
3. 错误信息保持一致
触发限流后,错误信息不应区分「密码错误」与「请求过多」,因为攻击者可以从响应中反推策略:
// Bad: leaks rate-limiting policy
{ "error": "Too many attempts. Try again in 215 seconds." }
// Good: doesn't leak information
{ "error": "Authentication failed. Please try again later." }
Autional 返回标准的 429 Too Many Requests 状态码与 Retry-After 响应头,但响应体与普通认证失败保持一致,不暴露限流细节。
4. 递进式惩罚
不要在第一次触发阈值时就封禁 24 小时,而应采用递进策略:
1st trigger: wait 1 minute
2nd trigger: wait 5 minutes
3rd trigger: wait 30 minutes
4th trigger: wait 2 hours + notify account owner
5th trigger: account temporarily locked, contact admin
这一策略对偶尔输错密码的正常用户尽量轻罚,同时对恶意攻击者逐级加大威慑。
5. 监控与告警
限流不是「配好就不管」的事。你需要:
- 监控限流触发频次(如果每天都在触发,可能需要调整阈值或排查原因)
- 监控被限流的 IP 数量(激增意味着正在被攻击)
- 监控被限流的账号数量(大量不同账号可能是撞库)
- 设置告警:限流触发率超过正常水平 10 倍时发出告警
总结
限流是身份安全的基础设施,不是可选的附加项。没有限流的登录端点就像一扇没有锁的门——只是还没被攻击者注意到。
Autional gateway-service 内置的分布式限流提供三层防护(IP 级、用户级、全局级),支持令牌桶与滑动窗口两种算法,并通过 Redis 实现跨实例的精确计数。每个租户都可以根据自身的安全需求与流量特征独立配置。
给你的登录端点穿上盔甲。
相关阅读
准备好给应用加上身份层了吗?
Autional 提供认证、组织、访问控制、审计与开发者 API —— 面向 AI 生成应用的身份层。