# 手机号密码认证独立代码复验报告 日期: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 与全量回归,可以进入独立复验后的精确暂存检查点。**