轻松聘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.
 
 
 

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 格式或短信登录接口。提交前仍需完成全量测试、敏感扫描、暂存和审查。