# 手机号+密码登录实施记录 ## 1. 范围 本批只实现手机号账号的密码认证闭环:登录、首次设置密码、已登录用户修改密码、短信验证后重置密码。未实现本机号码一键登录,也未改变既有短信登录、Access/Refresh Token 格式、评分、推荐或其他业务接口语义。 ## 2. 账号与密码策略 - 账号字段固定为 `users.phone`,要求 11 位数字;不引入用户名或邮箱登录。 - 密码使用 BCrypt 存储,成本沿用项目 `PasswordEncoder` 配置(当前为 10)。数据库不保存明文密码。 - 新密码长度为 12–64 个 Unicode code point;先做 NFC 规范化,不 trim、不自动小写。 - 拒绝常见弱密码、包含完整手机号的密码,以及与当前密码相同的密码。 - 不设置默认密码,不因密码登录自动创建账号。 ## 3. 接口 | 方法 | 路径 | 身份 | 作用 | | --- | --- | --- | --- | | POST | `/auth/password/login` | 匿名 | 手机号+密码登录并签发既有 Access/Refresh Token | | POST | `/auth/password/set` | Access Token | 首次设置密码,需要 `password-set` 短信验证码 | | POST | `/auth/password/change` | Access Token | 校验当前密码后修改密码 | | POST | `/auth/password/reset` | 匿名 | `password-reset` 短信验证码后重置密码 | 成功响应只返回现有认证所需结果或固定操作结果;错误响应不回显手机号、密码、验证码、Token、异常类或 SQL。 ## 4. 会话撤销 设置、修改、重置密码成功后调用现有 `AuthSessionService.revokeAllUserSessions`,撤销该用户全部会话,并写入固定动作审计。密码登录成功创建新的会话。密码修改时间写入 `users.password_changed_at`,为后续令牌失效策略保留服务端时间基准。 ## 5. 防枚举与限流 - 不存在手机号、停用用户、删除完成用户、错误密码统一返回 `CREDENTIALS_INVALID`(HTTP 401),不区分账号是否存在。 - 登录使用固定 dummy BCrypt hash 对未知手机号执行同类密码计算,避免明显时间差。 - Redis 记录仅保存 HMAC-SHA256 引用,不保存原始手机号或 IP。 - 同手机号连续 5 次失败锁定 15 分钟;同 IP 10 分钟最多 30 次尝试。 - Redis 不可用或密钥配置不满足要求时拒绝认证,不伪造登录成功。 ## 6. 配置 ```yaml app: auth: password: enabled: ${PASSWORD_LOGIN_ENABLED:false} rate-key-secret: ${PASSWORD_RATE_KEY_SECRET:} ``` 密码功能默认关闭;启用时必须提供至少 32 个字符的 `PASSWORD_RATE_KEY_SECRET`。生产环境不得把该密钥写入仓库或日志。 ## 7. 数据库 新增且仅新增 `V46__password_authentication.sql`,为 `users` 增加可空 `password_changed_at DATETIME`。未修改 V1–V45,未新增密码明文字段、Token 明文字段或其他表。 ## 8. 错误映射 - `CREDENTIALS_INVALID`(2005)→ 401。 - `PASSWORD_RATE_LIMITED`(2006)→ 429。 - `PASSWORD_STATE_CONFLICT`(2007)→ 409。 - `PASSWORD_POLICY_INVALID`(2008)→ 400。 - Redis 故障复用 `SERVICE_UNAVAILABLE`,响应为固定安全文案。 ## 9. 并发与事务 设置、修改、重置使用用户行悲观锁;密码更新、密码变更时间、会话撤销、审计在同一事务内完成。密码登录创建会话和 Refresh Token 轮换也先取得同一 `users` 行悲观锁,锁顺序统一为 `users -> auth_tokens`,从而避免改密提交后并发请求再次留下旧认证会话。 ## 10. 测试 - `PasswordPolicyTest`:长度、Unicode/NFC、常见密码、手机号包含、复用密码。 - `PasswordRateLimiterTest`:HMAC 不泄露原值、失败计数/锁定、Redis 故障 fail-closed、关闭开关行为。 - `PasswordAuthenticationIntegrationTest`:成功登录、错误凭据统一响应、修改密码撤销会话、短信重置后登录、精确夹具清理。 - 更新错误码映射契约测试与 OpenAPI 既有 DTO 安全断言,使断言只检查目标 DTO/响应,不因新增密码 schema 误报。 阻断整改后新增真实 Redis 原子计数/TTL 测试、真实 MySQL 登录/刷新与改密竞争测试,以及默认关闭与 Redis 故障失败关闭测试。密码专项与认证关联回归 **149/149**;最终 `mvn clean verify` **2825/2825,Failures=0,Errors=0,Skipped=0**。`python docs/verify_openapi.py` 通过,OpenAPI 为 **180 paths / 218 operations**。V46 已在全量 MySQL/Testcontainers 测试上下文中执行并通过 schema 校验。 ## 11. OpenAPI 静态契约由 176 paths / 214 operations 扩展为 **180 paths / 218 operations**,新增四个密码接口及对应请求/响应 schema。密码 DTO 不暴露 `userId`、`sessionId`、`role`、`passwordHash`、`token`、`secret` 等敏感字段。 ## 12. 阻断整改闭环 - 所有设置、修改、重置操作统一执行功能开关、Redis 可用性检查及每手机号 HMAC 引用 10 分钟最多 10 次的操作限流。 - 登录 IP 计数、密码失败计数与锁定全部改为 Redis Lua 原子脚本;计数与 TTL 在同一次执行中完成,并在 TTL 缺失时修复。锁使用 `SET ... NX`,并发迟到失败不会延长既有锁定期。 - 登录、会话创建、Refresh 轮换和密码变更共用 `users` 行悲观锁;真实 MySQL 竞争测试连续验证登录/刷新与改密终态,不留下旧认证活动会话。 - 密码配置统一由 `PasswordAuthenticationProperties` 承载,并对密钥、阈值和时间窗口进行启动期边界校验。 ## 13. 已知边界 - 验证码现有存储与消费语义保持不变,本批只保证密码认证链路不打印验证码;验证码摘要化另行立项。 - `password_changed_at` 已落库,但旧 Token 是否按该时间主动失效需在后续 Token 策略批次中单独验收。 ## 14. 变更边界 未修改评分、排名、推荐、邀约、申诉、消息撤回、简历上传、Flyway 旧迁移、既有 Token 格式或短信登录接口。提交前仍需完成全量测试、敏感扫描、暂存和审查。