轻松聘AI开发
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

6.1 KiB

手机号密码认证独立代码复验报告

日期:2026-09-20 结论:FAIL,不进入暂存/提交检查点

1. 复验范围

复核了密码认证生产代码、V46、SecurityConfig、错误映射、DTO、配置和现有测试;未修改生产代码、未暂存、未提交、未推送。

2. 已通过项目

  • 账号字段为 users.phone,无用户名/邮箱密码登录入口。
  • BCrypt 摘要存储,未发现明文密码、Token 或 Redis 原始手机号/IP 写入日志、响应、审计的代码路径。
  • 登录对未知手机号使用 dummy BCrypt 摘要,错误凭据统一使用 CREDENTIALS_INVALID(2005)。
  • 设置、修改、重置使用 findByIdForUpdate,成功后调用既有全量会话撤销。
  • V46 是唯一新增迁移,仅增加可空 users.password_changed_at;V1–V45 未修改。
  • 密码接口已进入 SecurityConfig,OpenAPI 为 180 paths / 218 operations。
  • 定向测试:356/356,Failures=0、Errors=0、Skipped=0。
  • 全量 mvn clean verify:2818/2818,Failures=0、Errors=0、Skipped=0。
  • OpenAPI 校验:All checks passed!。

3. 阻断项

F1:设置/修改密码绕过 Redis 失败关闭和功能开关

PasswordAuthenticationService.setPassword(约 69 行)与 changePassword(约 85 行)没有调用 rateLimiter.checkAllowed 或等价的启用检查;只有 login 和 resetPassword 调用了它。

因此在 PASSWORD_LOGIN_ENABLED=false 或 Redis 不可用时,已认证用户仍可能执行设置/修改密码,违反设计 §9.4“密码敏感操作失败关闭”,也使“默认关闭”没有覆盖全部密码操作。

F2:没有实现密码敏感操作的独立 10 分钟限流

设计要求设置/修改/重置每手机号引用 10 分钟最多 10 次。当前实现只有登录用的 IP 30/10 分钟和手机号失败锁定;setPassword、changePassword、resetPassword 没有操作计数、失败计数或独立阈值。

F3:限流计数不是原子实现

PasswordRateLimiter 使用 increment 后再单独 expire(约 52–55、70–73 行),没有 Lua 脚本或等价原子操作。并发下可能出现计数与 TTL 不一致,且存在永久 key 风险,违反设计 §9.3。

F4:密码变更与登录/刷新没有共用锁或版本栅栏

  • 密码登录通过 userRepository.findByPhone 读取用户后直接调用 createSession。
  • AuthSessionService.createSession 未对 users 行加锁。
  • Refresh 路径的 requireUsableUser 使用普通 findById。
  • password_changed_at 只写入,未在登录、刷新或会话校验中读取。

因此“登录/刷新与改密并发”可能出现改密撤销之后又创建旧认证会话;现有测试没有证明设计要求的并发终态。

4. 非阻断观察

  • 初次复验时,bcrypt-strength、阈值等设计配置仍为硬编码常量;本轮已将限流阈值与时间窗口收束到带启动期边界校验的配置属性。BCrypt 成本仍沿用项目统一 PasswordEncoder 配置,不在本轮另设第二套参数源。
  • 验证码明文存储沿用既有语义,属于设计中明确保留的后续事项。

5. 下一步整改要求

  1. 为所有密码敏感操作统一增加启用检查、Redis fail-closed 和独立操作限流。
  2. 用 Lua 或 Redis 原子脚本完成“计数 + 首次 TTL”一体化;补并发增量测试。
  3. 为登录、刷新、改密建立统一 users 行锁/密码版本栅栏,补真实并发测试,证明改密提交后不会留下旧会话。
  4. 补齐 Redis 故障、开关关闭、10 分钟操作限流、并发登录/刷新/改密测试,再重新执行全量和真实 MySQL/Redis 验收。

6. Git 边界

  • 暂存区为空;未提交、未推送。
  • 本报告为独立复验记录,不包含生产修复。
  • 历史未跟踪验收产物保持不动。

7. 阻断整改复验(2026-09-20)

初次 FAIL 及 F1–F4 证据保留不变。针对四项阻断完成定向整改后重新复验,结果如下:

F1:PASS——所有密码操作统一失败关闭

  • setPassword、changePassword、resetPassword 均调用 checkOperationAllowed。
  • 功能默认关闭时,直接调用修改密码也返回固定 SERVICE_UNAVAILABLE;Redis 不可用时同样安全失败,密码摘要和既有会话均不变化。
  • 配置改由 PasswordAuthenticationProperties 统一承载并在启动期校验。

F2:PASS——独立操作限流已实现

  • 设置、修改、重置共用按手机号 HMAC 引用计数的操作限流,默认 10 分钟最多 10 次,第 11 次返回 429。
  • Redis key、异常与日志均不包含原始手机号、密码或 Token。

F3:PASS——Redis 原子计数与锁定已实现

  • 登录 IP 计数通过 Lua 在一次 Redis 执行中完成 INCR 与 TTL 设置/修复。
  • 密码失败计数和锁定通过另一 Lua 脚本原子完成;锁使用 NX,不会被并发迟到请求反复延长。
  • 真实 Redis 7 Testcontainers 测试验证 20 路并发累计精确为 20,计数键和锁键均有 TTL,且达到阈值后登录被拒绝。

F4:PASS——登录、刷新与改密共用数据库栅栏

  • 密码登录按手机号悲观锁定 users 行;createSession 再按 id 锁定用户。
  • Refresh 先锁定 users 行,再查询和消费 auth_tokens;密码修改使用相同的 users -> auth_tokens 锁顺序。
  • 真实 MySQL 8.4 Testcontainers 竞争测试各执行 10 轮:登录与改密、刷新与改密的最终状态均不留下旧认证活动会话,最终密码为新摘要。

8. 最终验证

  • 密码专项及认证关联回归:149/149,Failures=0、Errors=0、Skipped=0。
  • 最终 mvn clean verify:2825/2825,Failures=0、Errors=0、Skipped=0;真实 MySQL 8.4 与 Redis 7 Testcontainers 均实际执行。
  • OpenAPI:180 paths / 218 operations,All checks passed!。
  • git diff --check 无错误;暂存区为空;未提交、未推送;V1–V45 未修改,V46 仍为唯一新增迁移。

9. 最终结论

PASS——F1–F4 四个阻断均已修复并通过定向、并发、真实 Redis/MySQL 与全量回归,可以进入独立复验后的精确暂存检查点。