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. 下一步整改要求
- 为所有密码敏感操作统一增加启用检查、Redis fail-closed 和独立操作限流。
- 用 Lua 或 Redis 原子脚本完成“计数 + 首次 TTL”一体化;补并发增量测试。
- 为登录、刷新、改密建立统一 users 行锁/密码版本栅栏,补真实并发测试,证明改密提交后不会留下旧会话。
- 补齐 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 与全量回归,可以进入独立复验后的精确暂存检查点。