5.9 KiB
手机号+密码登录实施记录
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. 配置
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 格式或短信登录接口。提交前仍需完成全量测试、敏感扫描、暂存和审查。