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 场景。
  • 限流日志可以区分全局匿名、认证端点、账号失败和已登录用户四类触发来源,且不泄漏敏感信息。

关联

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions