Skip to content

[Bug]: 认证限流共享 IP 计数桶导致正常登录被误拦截 #292

Description

@huyanxius

现象

用户在正常登录过程中,连续登录 2–3 次后可能收到「请求过于频繁,请稍后再试」或「请求过于频繁」,即使邮箱和密码均正确。

该现象不是登录面板重复提交造成的。前端提交期间会锁定按钮,已有测试也断言一次表单提交只调用一次登录 API。问题来自后端限流计数桶的粒度与执行顺序。

已验证复现

验证基线:upstream/main@270227135d9b1ebdc9864ac0946ee8fc64087690。使用独立 worktree、独立 Redis 和本地测试账号,不复用生产数据。

场景一:干净窗口

同一 IP 连续调用 POST /auth/login

  • 第 1–10 次:登录成功,业务码 200
  • 第 11 次:业务码 429,提示「请求过于频繁,请稍后再试」

这符合当前「敏感接口单 IP 10 次/分钟」的实现。

场景二:同一 IP 已有其他认证请求

先在同一计数窗口内发出 8 次其他敏感认证请求,再使用正确密码登录:

  • 第 1 次登录:成功
  • 第 2 次登录:成功
  • 第 3 次登录:业务码 429,提示「请求过于频繁,请稍后再试」
  • Redis 中敏感接口计数为 11

参数校验失败的认证请求同样会占用额度,因为限流发生在路由参数校验和业务处理之前。

场景三:同一 IP 已有普通 API 流量

先在同一计数窗口内发出 58 次普通 API 请求,再使用正确密码登录:

  • 第 1 次登录:成功
  • 第 2 次登录:成功
  • 第 3 次登录:业务码 429,提示「请求过于频繁」
  • Redis 中全局计数为 61

因此,「登录 2–3 次就触发限流」可以由当前主仓代码稳定复现;具体线上一次命中的是敏感接口桶还是全局桶,需要结合生产 windup.ratelimit 日志或 Redis 计数确认,但不影响缺陷本身成立。

根因

RateLimitMiddleware 当前同时存在三层问题:

  1. 不同认证动作共用同一个 IP 桶。 /auth/register/auth/login/auth/send-code/auth/login-by-code/auth/reset-password 全部写入 ratelimit:sensitive:{ip},合计只有 10 次/分钟。
  2. 普通 API 与登录共享全局 IP 桶。 所有请求都会先写入 ratelimit:api:{ip},上限 60 次/分钟。页面加载产生的业务请求会挤占后续登录额度。
  3. 用户级限流实际上拿不到用户身份。 请求顺序是 RateLimitMiddleware → AuthMiddleware → route,但用户级检查在 call_next 之前读取 request.state.current_user。此时鉴权尚未执行,user_id 为空,所以声明的 120 次/分钟用户桶没有真正覆盖已登录请求。

此外,密码登录服务本身已经具备按邮箱记录错误密码的保护:连续 5 次失败后锁定 15 分钟;发验证码也已经有按邮箱 60 秒冷却。当前 IP 共享桶叠加在这些细粒度保护之外,主要增加了正常用户被误伤的概率。

影响

  • 同一用户在登录、注册、发码等操作之间切换时,会互相消耗额度。
  • 办公网、校园网、家庭 NAT 等共享出口 IP 下,不同用户会互相消耗额度。
  • 已有页面流量可能导致后续正确登录被拒绝。
  • 若生产代理链未正确还原客户端 IP,误伤范围会扩大到代理后的全部用户。
  • 当前实现与认证模块原本描述的「账号级限流防暴力破解」不一致。

期望行为

  • 正确密码登录不应因为同一 IP 上此前的普通 API、发码、注册或其他账号操作,在第 2–3 次就被拒绝。
  • 密码暴力破解、验证码滥发和匿名流量攻击仍应分别受到限制,不能通过删除限流或单纯大幅调高阈值解决。
  • 共享出口 IP 下,不同账号的正常认证操作不应直接共用一个过小的账号保护额度。
  • 已登录请求应真正进入用户级限流;匿名请求保留合理的 IP 级兜底。

建议修改边界

  1. 将密码登录、发码、注册、验证码登录、重置密码拆分为独立限流策略,不再共用一个 10 次/分钟的敏感接口桶。
  2. 密码登录以失败尝试为主要保护信号,至少同时具备账号维度与 IP 维度的防护;成功登录不消耗账号的错误密码额度,并保留更高的粗粒度防滥用上限。
  3. 保留现有按邮箱的错误密码锁定和发码冷却,避免出现两套含义重叠但阈值不一致的计数。
  4. 调整限流与鉴权的数据流,使已登录请求能按 user_id 计数;匿名请求再退回 IP 维度。
  5. 为各限流分支记录可区分的日志信息,但不要记录邮箱、token 等敏感值。

本 Issue 不包含登录页 UI 改版、认证提示文案重写、JWT 时效调整,也不改变现有业务响应 envelope。

验收标准

  • 同一 IP 在一分钟内已有至少 8 次其他认证端点请求后,连续 3 次正确密码登录仍可成功,不会命中共享敏感接口桶。
  • 同一 IP 在一分钟内已有至少 58 次普通 API 请求后,连续 3 次正确密码登录仍不会被全局匿名桶误拦截。
  • 两个不同邮箱位于同一 IP 时,不共享账号级错误密码计数;同时保留可验证的 IP 级攻击上限。
  • 错误密码达到既定阈值后仍会触发保护,正确密码成功后错误计数按现有契约清理。
  • /auth/send-code 的单邮箱 60 秒冷却仍然生效,且不会消耗密码登录额度。
  • 自动化测试证明已登录请求能够实际进入用户级限流分支,而不是因中间件顺序始终跳过。
  • 自动化测试覆盖直连、可信代理 X-Forwarded-For 与共享出口 IP 场景。
  • 限流日志可以区分全局匿名、认证端点、账号失败和已登录用户四类触发来源,且不泄漏敏感信息。

关联

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions