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

26 KiB

手机号密码认证设计

文档类型:设计说明与实现参考 状态:设计已确认,可进入实施;尚未编码 适用项目:mydreamjob-server 基线:master / dedd47e3db42fee26bbeb21f1925ca1da127fb65 设计日期:2026-09-20

1. 目标

在不破坏现有手机号验证码登录的前提下,增加手机号密码认证能力:

  1. 手机号密码登录;
  2. 首次设置密码;
  3. 使用旧密码修改密码;
  4. 使用短信验证码找回并重置密码;
  5. 密码错误计数、临时锁定和手机号/IP 双维度限流;
  6. 密码变更后撤销该用户全部登录会话;
  7. 防止账号枚举、时序侧信道以及日志、审计和响应泄漏。

所有登录方式认证成功后必须复用现有 AuthSessionService 创建 Access/Refresh Token 会话,不建设第二套 Token 或会话体系。

2. 非目标

本模块明确不包含:

  • 本机号码一键登录;
  • 微信、支付宝、Apple、OAuth 或其他第三方登录;
  • 用户名或邮箱登录;
  • 支付、兑换、实名或多因素认证;
  • 修改现有 Access/Refresh Token 格式、有效期、轮换和重放处置;
  • 修改验证码登录的自动注册语义;
  • 修改角色、权限、注销、评分、推荐、简历或消息业务语义;
  • 在本轮接入外部弱密码或泄漏密码在线查询服务。

3. 当前基线

3.1 已有能力

能力 当前实现
验证码发送 POST /auth/sms/send
验证码登录 POST /auth/login
登录会话 AuthSessionService#createSession
Token 刷新 Refresh Token 单次消费、轮换与重放撤销
登出 撤销当前 session
全局撤销 AuthSessionService#revokeAllUserSessions
密码字段 users.password_hash VARCHAR(255) NULL
密码算法 已有 BCryptPasswordEncoder Bean
临时状态 已有 Redis 及 RedisTemplate<String,Object>
用户状态 is_active、注销记录和注销手机号墓碑

3.2 当前缺口

  • password_hash 没有面向普通用户的写入或校验入口;
  • 没有密码设置、修改、重置 DTO 和服务;
  • 没有密码失败计数、账号锁定或 IP 限流;
  • 没有密码变更后的会话撤销审计;
  • CREDENTIALS_INVALID(2005) 当前未被使用,且未显式映射为 HTTP 401;
  • 现有通用 RATE_LIMITED(9002) 保持历史 HTTP 400,不适合作为新密码接口的 429 契约。

4. 核心决策

事项 决策
账号标识 唯一账号为 11 位手机号
登录方式 验证码登录和密码登录并存
未设置密码的旧用户 继续使用验证码登录,再主动设置密码
密码长度 12–64 个 Unicode 字符(按 Unicode code point 计)
机械复杂度 不强制大小写、数字、符号组合
弱密码 本地、版本化弱密码名单整值匹配;禁止包含完整手机号
规范化 设置与校验前统一做 Unicode NFC;不 trim、不改大小写
存储 BCrypt 单向摘要,禁止明文、可逆加密或独立盐字段
BCrypt cost 默认 10;部署机基准测试后才允许调整
首次设置 有效 Access Token + password-set 短信验证码
修改 有效 Access Token + 当前密码
重置 手机号 + password-reset 短信验证码
会话 设置、修改、重置成功后撤销全部会话,包括当前会话
登录失败锁定 手机号引用连续失败 5 次,锁定密码登录 15 分钟
IP 限流 单 IP 10 分钟最多 30 次密码登录尝试
Redis 故障 密码登录及密码敏感操作失败关闭,返回服务暂不可用
防枚举 不存在、无密码、密码错误、inactive、注销均返回相同 401
注册 密码登录绝不自动创建账号

5. API 契约

所有路径继续受全局 context path /api/v1 约束。下面只写 Controller 相对路径。

5.1 密码登录

POST /auth/password/login
Content-Type: application/json
{
  "phone": "13800138000",
  "password": "correct horse battery staple"
}

成功时返回与现有验证码登录相同的用户与 Token 字段:

{
  "code": 200,
  "message": "success",
  "data": {
    "userId": 123,
    "phone": "13800138000",
    "nickname": "用户8000",
    "role": "STUDENT",
    "accessToken": "<access-token>",
    "refreshToken": "<refresh-token>",
    "expiresIn": 86400
  },
  "requestId": "..."
}

约束:

  • phone 必须为 11 位数字;
  • password 必填,输入上限 64 个字符;
  • 不存在的手机号不得创建用户;
  • 失败原因统一,不返回“用户不存在”“尚未设置密码”“账号停用”等差异信息;
  • 成功后清除该手机号引用的失败计数,但不清除 IP 窗口计数。

5.2 首次设置密码

POST /auth/password/set
Authorization: Bearer <access-token>
Content-Type: application/json
{
  "smsCode": "123456",
  "newPassword": "correct horse battery staple"
}

前置步骤:

POST /auth/sms/send?phone=<当前登录手机号>&purpose=password-set

规则:

  • 手机号只能来自 Access Token 对应的当前用户,客户端不得传入手机号;
  • 验证码用途必须严格为 password-set;
  • 仅 password_hash IS NULL 时允许;
  • 已有密码返回状态冲突,不得覆盖;
  • 成功后撤销全部会话,调用方当前 Access Token 随即失效;
  • 成功响应不签发新 Token。

成功响应:

{
  "code": 200,
  "message": "success",
  "data": { "passwordConfigured": true, "reauthenticationRequired": true },
  "requestId": "..."
}

5.3 修改密码

POST /auth/password/change
Authorization: Bearer <access-token>
Content-Type: application/json
{
  "currentPassword": "old password",
  "newPassword": "new correct horse battery staple"
}

规则:

  • 只接受 Access Token,Refresh Token 必须返回 401;
  • 当前密码错误统一返回凭据错误;
  • 未设置密码时返回密码状态冲突,引导用户走首次设置;
  • 新旧密码不得相同;
  • 成功后撤销全部会话且不签发新 Token。

5.4 找回并重置密码

前置步骤:

POST /auth/sms/send?phone=13800138000&purpose=password-reset

重置:

POST /auth/password/reset
Content-Type: application/json
{
  "phone": "13800138000",
  "smsCode": "123456",
  "newPassword": "new correct horse battery staple"
}

规则:

  • 该接口匿名可访问;
  • 验证码用途必须严格为 password-reset;
  • 请求短信验证码时,无论账号是否存在都返回相同的受理响应;
  • 账号不存在时不得创建账号;
  • 只有有效验证码和可用账号才能修改摘要;
  • 成功后撤销全部会话且不自动登录;
  • inactive 或已完成注销的账号不得被重置流程复活;
  • 验证失败不说明是账号、验证码还是用户状态问题。

6. DTO 与输入处理

建议新增:

  • PasswordLoginRequest(phone, password);
  • PasswordSetRequest(smsCode, newPassword);
  • PasswordChangeRequest(currentPassword, newPassword);
  • PasswordResetRequest(phone, smsCode, newPassword);
  • PasswordOperationResponse(passwordConfigured, reauthenticationRequired)。

要求:

  • DTO 不实现 toString(),不得使用会输出字段值的 Lombok @Data;
  • 密码字段不进入 Bean Validation 默认错误回显;
  • JSON 未知字段继续遵循项目现有全局策略;
  • Controller 不打印请求体,不把请求 DTO 放入异常或审计 details;
  • 前端可提供 confirmPassword,但后端不信任该字段;后端只接收一次新密码。

7. 密码策略

7.1 接受规则

  1. NFC 规范化后长度为 12–64 个 Unicode code point;
  2. 允许空格和 Unicode 字符;
  3. 不自动删除首尾空格;
  4. 不自动转换大小写;
  5. 不能等于或包含完整手机号;
  6. 不能命中项目内置、版本化的弱密码集合;
  7. 新密码不能与当前密码相同。

7.2 弱密码名单

第一版使用仓库内只读资源,例如:

src/main/resources/security/common-passwords.txt

该文件:

  • 只包含公开常见弱密码,不包含任何用户数据;
  • 在启动时加载为不可变集合;
  • 加载失败时应用启动失败,不能静默关闭策略;
  • 后续更新必须作为可审查的版本化变更;
  • 本轮不调用外部泄漏密码 API,避免把用户密码或派生值发送给第三方。

8. BCrypt 存储与升级

8.1 写入

normalized = NFC(rawPassword)
password_hash = BCryptPasswordEncoder.encode(normalized)

禁止:

  • 明文密码落库;
  • SHA/MD5 等快速摘要;
  • 在数据库增加明文盐;
  • 把密码、摘要或其前缀写入日志、审计、响应、消息或异常。

8.2 校验

PasswordEncoder.matches(NFC(input), storedHash)

手机号不存在或没有密码时,也必须对固定 dummy BCrypt 摘要执行一次 matches,减少响应时间差异。dummy 摘要作为应用常量,由构建时生成,不包含真实密码或部署秘密。

8.3 cost 与升级

  • 默认沿用当前 BCryptPasswordEncoder strength 10;
  • 在 Jenkins 部署目标机上测量 P50/P95,再决定是否提高;
  • 目标单次校验不超过服务可接受的登录延迟和并发预算;
  • 若未来启用 upgradeEncoding(),仅在一次成功密码登录中升级摘要,并与会话创建保持失败原子性;第一版可不启用自动升级。

9. 失败计数、锁定与限流

9.1 标识保护

Redis key 不得包含完整手机号或原始 IP。使用独立 HMAC 密钥生成不可逆引用:

phoneRef = HMAC-SHA256(PASSWORD_RATE_KEY_SECRET, normalizedPhone)
ipRef    = HMAC-SHA256(PASSWORD_RATE_KEY_SECRET, normalizedIp)

只使用截断后的十六进制引用作为 key 片段;HMAC 原始值不得进入日志。

9.2 Redis key

auth:pwd:fail:{phoneRef}        TTL 15 分钟
auth:pwd:locked:{phoneRef}      TTL 15 分钟
auth:pwd:ip:{ipRef}:{window}    TTL 10 分钟
auth:pwd:op:{phoneRef}:{kind}   敏感操作限流

9.3 规则

维度 规则 超限行为
手机号 连续失败 5 次 锁定密码登录 15 分钟
IP 10 分钟最多 30 次密码登录请求 HTTP 429
设置/修改/重置 每手机号引用 10 分钟最多 10 次 HTTP 429
登录成功 清除手机号失败和锁定 key 保留 IP 窗口
验证码登录 不受密码锁定影响 仍受原短信限流

计数与首次 TTL 设置必须由 Lua 脚本或等价 Redis 原子操作完成,不能用非原子的 INCR 后再独立 EXPIRE 留下永久 key。

9.4 Redis 故障

密码认证与密码敏感操作在 Redis 不可用时失败关闭:

  • 不校验密码;
  • 不创建会话;
  • 不修改密码;
  • 返回固定的服务暂不可用响应;
  • 日志只记录固定分类和 requestId,不记录 Redis 地址、手机号或原始异常消息。

验证码登录沿用既有行为,本设计不顺带改变其 Redis 降级语义。

10. 防账号枚举

密码登录以下情况使用完全相同的外部响应:

  • 手机号不存在;
  • password_hash IS NULL;
  • 密码错误;
  • 用户 inactive;
  • 用户已完成注销;
  • 手机号命中注销墓碑;
  • 用户角色或会话创建条件不合法。

统一响应:

HTTP/1.1 401 Unauthorized
{
  "code": 2005,
  "message": "手机号或密码错误",
  "requestId": "..."
}

实现要求:

  • 不存在用户仍执行 dummy BCrypt;
  • inactive 等状态不得在校验前快速返回;
  • 响应状态、业务码、正文结构和主要计算路径一致;
  • 不承诺网络层严格恒定耗时,但独立测试需验证不存在明显快速分支;
  • 找回密码的发送入口统一返回“如账号可用,验证码将发送”。

11. 状态机

未设置密码
  ├─ 有效 Access + password-set 验证码 ─→ 已设置密码
  └─ password-reset 验证码 ────────────→ 已设置密码

已设置密码
  ├─ 手机号 + 正确密码 ────────────────→ 创建登录会话
  ├─ 当前密码 + 新密码 ────────────────→ 已修改,全部会话撤销
  ├─ password-reset 验证码 ────────────→ 已重置,全部会话撤销
  └─ 再次 set ─────────────────────────→ 409 状态冲突

inactive / 注销完成
  └─ 任何密码操作 ─────────────────────→ 统一拒绝,不复活账号

12. 事务与会话边界

12.1 密码登录

Controller(无事务)
  → 检查 Redis IP/手机号限制
  → PasswordAuthenticationService.login(REQUIRED)
      → 查询用户
      → BCrypt 真/假摘要校验
      → 校验用户可用性与注销状态
      → AuthSessionService.createSession
      → 写成功审计
  → 事务提交后清理手机号失败计数

会话记录保存失败时,登录事务回滚且不得返回 Token。

12.2 设置、修改、重置

每个操作必须在单个 REQUIRED 事务内完成:

锁定用户行/进行带版本条件更新
  → 验证当前身份或原密码/短信验证码
  → 校验新密码策略
  → 写 password_hash(及 password_changed_at,如批准)
  → revokeAllUserSessions(reason=...)
  → 写脱敏审计
  → 提交

任一步失败必须整体回滚。特别是:

  • 验证码不能已消费而密码未更新;
  • 密码不能已更新而旧会话仍活动;
  • 审计失败时是否回滚沿用现有 AuditService 的事务行为;
  • 成功响应只可在事务提交后返回。

12.3 会话撤销原因

新增白名单原因:

PASSWORD_SET
PASSWORD_CHANGED
PASSWORD_RESET

不得把手机号、密码、验证码或操作来源拼入 revocation_reason。

13. 并发与幂等

场景 预期
两次并发首次设置 恰好一次成功,另一次 409;最终只有一个摘要
两次并发修改 使用数据库行锁或乐观版本保证旧密码只成功一次
两次并发重置 同一验证码只能消费一次;恰好一次成功
修改密码与 Refresh 并发 密码事务提交后的终态必须无活动旧会话
登录与密码修改并发 密码修改提交后不得留下使用旧密码创建的活动会话
重复成功请求 不得重复写审计、重复创建会话或返回同一 Token

仅调用现有 revokeAllUserSessions 不足以自动证明“登录/刷新与密码变更并发后无活动会话”。实现阶段必须设计数据库锁顺序或安全版本栅栏,并用真实 MySQL 并发测试证明终态。禁止使用 synchronized、JVM 锁或 Thread.sleep 作为正确性机制。

推荐实现:密码变更事务对 users 行加写锁;登录和 Refresh 在创建后继会话前以同一锁顺序读取可用用户。若评估发现会扩大 S02 锁竞争,必须先补独立事务设计,不得直接上线。

14. 数据模型

14.1 现有字段

继续使用:

users.password_hash VARCHAR(255) NULL

NULL 表示尚未设置密码,不代表账号不可用;用户仍可使用验证码登录。

14.2 建议新增字段

建议新增:

password_changed_at DATETIME NULL

用途:

  • 记录密码凭据最后变更时间;
  • 支持安全审计和后续 Token 签发时间比较;
  • 不从 updated_at 推断,因为资料修改也会更新该字段。

是否最终新增必须在实施前检查当前最新 Flyway 后再分配迁移号。不得修改 V1–V45,也不得在设计文档预占 V46。

不建议把登录失败计数、锁定结束时间写入 MySQL;这些临时状态放 Redis,避免热点行写放大。

15. 错误契约

建议复用和新增如下错误:

场景 HTTP ErrorCode 对外消息
手机号或密码认证失败 401 复用 CREDENTIALS_INVALID(2005) 手机号或密码错误
登录/操作限流 429 新增 PASSWORD_RATE_LIMITED 尝试次数过多,请稍后再试
密码策略不合格 400 新增 PASSWORD_POLICY_INVALID 密码不符合安全要求
已设置/未设置等认证后状态冲突 409 新增 PASSWORD_STATE_CONFLICT 当前密码状态不允许此操作
当前密码错误 401 CREDENTIALS_INVALID(2005) 手机号或密码错误
Redis 等认证依赖不可用 503 复用或新增经批准的服务不可用码 认证服务暂不可用
DTO 格式错误 400 BAD_REQUEST(1001) bad request

要求:

  • 将 CREDENTIALS_INVALID 显式映射为 401;其当前无生产调用点,实施时仍需做全仓回归;
  • 不改变通用 RATE_LIMITED(9002) 的历史 400 映射;
  • ErrorCode 数字必须在实施时从当前主线分配,表中的新增名称不预定数字;
  • 匿名接口不得返回 USER_NOT_FOUND、USER_DISABLED 或 ACCOUNT_DELETED。

16. 日志与审计

允许的审计 action:

PASSWORD_LOGIN_SUCCEEDED
PASSWORD_LOGIN_FAILED
PASSWORD_LOGIN_RATE_LIMITED
PASSWORD_SET
PASSWORD_CHANGED
PASSWORD_RESET
AUTH_SESSIONS_REVOKED

审计规则:

  • 登录失败若账号不可识别,user_id 可为 null;
  • details 只包含固定分类,如 INVALID_CREDENTIALS、RATE_LIMITED;
  • 手机号仅允许不可逆 phoneRef,不允许完整或掩码手机号进入审计 details;
  • 不记录密码、摘要、验证码、JWT、Redis key、SQL 参数或异常 cause;
  • 应限制失败审计写入频率,防止攻击者制造审计表写放大;聚合限流审计优先于逐次落库;
  • 日志只记录固定文本、requestId 和固定错误分类。

17. SecurityConfig

匿名放行:

POST /auth/password/login
POST /auth/password/reset

需要有效 Access Token:

POST /auth/password/set
POST /auth/password/change

规则:

  • 不能使用宽泛的 /auth/** permitAll;
  • Refresh Token 访问 set/change 必须为 401;
  • STUDENT、COMPANY、ADMIN 均可管理自己的密码;
  • 角色不参与密码认证成功与否,只进入 Token 的既有 role claim;
  • 不接受 userId、sessionId 或角色作为请求参数。

18. 配置

建议配置:

app:
  auth:
    password:
      enabled: ${PASSWORD_LOGIN_ENABLED:false}
      bcrypt-strength: ${PASSWORD_BCRYPT_STRENGTH:10}
      min-length: ${PASSWORD_MIN_LENGTH:12}
      max-length: ${PASSWORD_MAX_LENGTH:64}
      max-failures: ${PASSWORD_MAX_FAILURES:5}
      lockout-seconds: ${PASSWORD_LOCKOUT_SECONDS:900}
      ip-window-seconds: ${PASSWORD_IP_WINDOW_SECONDS:600}
      ip-max-attempts: ${PASSWORD_IP_MAX_ATTEMPTS:30}
      operation-max-attempts: ${PASSWORD_OPERATION_MAX_ATTEMPTS:10}
      rate-key-secret: ${PASSWORD_RATE_KEY_SECRET}

约束:

  • enabled 默认 false,发布前显式开启;
  • rate-key-secret 无默认值,启用密码模块时缺失或过短必须启动失败;
  • 密钥存放 Jenkins Credentials,不写入 Git、日志或命令历史;
  • 阈值配置需做范围校验,不能允许 0、负数或极端值关闭保护;
  • 配置错误只显示配置名,不显示值。

19. 组件建议

建议新增或调整:

auth/password/
  PasswordAuthenticationService
  PasswordPolicy
  PasswordRateLimiter
  PasswordIdentifierHasher
  PasswordAuthenticationProperties
  PasswordAuditService(可复用既有安全审计封装时不新增)

auth/dto/
  PasswordLoginRequest
  PasswordSetRequest
  PasswordChangeRequest
  PasswordResetRequest
  PasswordOperationResponse

职责边界:

  • Controller:接收 DTO、提取受信身份与请求上下文、包装响应;
  • Service:认证编排、事务、用户状态和会话处理;
  • Policy:纯密码规则,不访问数据库;
  • RateLimiter:只操作 Redis,不查询用户;
  • IdentifierHasher:生成 Redis/审计不可逆引用;
  • AuthSessionService:继续作为唯一 Token 会话写入口。

不得把所有逻辑继续堆入现有 AuthService。

20. 自动化测试

20.1 密码策略

  • 11/12/64/65 字符边界;
  • Unicode code point 与 NFC;
  • 首尾空格不被 trim;
  • 中文、空格和符号可用;
  • 手机号包含检测;
  • 弱密码拒绝;
  • 新旧密码相同拒绝;
  • 测试失败信息不包含密码。

20.2 密码登录

  • 已设置密码登录成功并创建一次会话;
  • 不存在、未设置、错误、inactive、注销、角色异常全部 401/2005;
  • 六类失败响应 JSON 完全一致;
  • 不存在账号执行 dummy BCrypt 的变异门禁;
  • 密码登录不自动注册;
  • Access/Refresh 类型、摘要存储和现有 S02 语义不变;
  • 登录事务失败时无 Token、无活动会话。

20.3 设置、修改、重置

  • 首次设置成功;重复设置 409;
  • password-set 与其他用途验证码不可互换;
  • 修改密码需正确旧密码;
  • 重置需 password-reset 验证码;
  • 验证码成功后原子消费;
  • inactive/注销账号不可设置或重置;
  • 成功后全部旧 Access/Refresh 均失效;
  • 不自动签发新 Token;
  • 数据库只保存 BCrypt 摘要。

20.4 限流与故障

  • 第 1–4 次失败未锁定,第 5 次锁定;
  • 锁定 TTL 精确且不被后续请求无限延长;
  • 成功登录清除手机号失败计数;
  • IP 限流跨多个手机号生效;
  • 手机号锁定不阻止验证码登录;
  • Redis key 无完整手机号/IP;
  • Redis 不可用时无登录、无修改、无会话;
  • Lua/原子计数并发下不丢增量。

20.5 并发

  • 并发 set 恰好一个成功;
  • 并发 reset 同一验证码恰好一个成功;
  • password change 与 refresh 竞争终态无旧活动会话;
  • password change 与 password login 竞争终态符合锁协议;
  • 不允许 catch-all 吞掉异常,不使用 sleep 证明正确性。

20.6 脱敏和副作用

  • 真实 Logback 捕获 root、应用、Hibernate、JDBC logger;
  • 扫描 message、throwable、cause 和 SQL 绑定参数;
  • 响应、日志、审计、通知、消息、RabbitMQ 无密码/摘要/验证码/Token/完整手机号;
  • 认证失败不修改业务表;
  • 密码操作不发送通知或 RabbitMQ 消息,除非另行设计。

21. 真实 MySQL/Redis/JAR 黑盒验收

必须使用最新 JAR、真实 MySQL 与 Redis,至少覆盖:

  1. Flyway 执行与 Hibernate validate;
  2. 验证码旧用户设置密码并重新登录;
  3. 密码登录成功及 Access/Refresh 类型矩阵;
  4. 不存在、无密码、错误、inactive、注销的统一响应;
  5. 5 次失败与 15 分钟锁定;
  6. 单 IP 跨手机号限流;
  7. Redis 停止后失败关闭,恢复后正常;
  8. 设置、修改、重置后三类旧会话全部失效;
  9. 并发设置、重置、登录/修改、Refresh/修改;
  10. 数据库搜索密码原文和 Token 原文命中 0;
  11. 日志、审计和响应敏感扫描命中 0,扫描器需注入探针证明有效;
  12. 关键业务表逐字段副作用快照;
  13. RabbitMQ ready/unacked 保持 0;
  14. 静态与运行时 OpenAPI 路径和 operation 集合一致;
  15. 全量 mvn clean verify 为 Failures=0、Errors=0、Skipped=0。

22. Apifox 与前端联调

前端登录页展示:

  • 验证码登录;
  • 密码登录;
  • 忘记密码;
  • 已验证码登录用户的“设置密码”;
  • 已设置密码用户的“修改密码”。

前端不得根据不同错误文案推断账号状态。密码登录失败统一显示“手机号或密码错误”;限流显示通用等待提示。设置、修改或重置成功后:

  1. 清除本地 Access/Refresh Token;
  2. 跳转登录页;
  3. 提示“密码已更新,请重新登录”。

Apifox 环境继续使用统一 baseUrl,密码登录成功后复用现有后置脚本保存 Access/Refresh Token。

23. 发布和兼容

建议顺序:

  1. 完成设计评审;
  2. 从合并时主线确定实际 Flyway 编号;
  3. 先部署数据库兼容迁移;
  4. 部署后端但保持 PASSWORD_LOGIN_ENABLED=false;
  5. 验证验证码登录和旧客户端不受影响;
  6. 配置 Jenkins Credentials 与限流参数;
  7. 小范围启用密码入口;
  8. 完成 Apifox 与前端联调;
  9. 观察登录成功率、限流率、Redis 和 BCrypt 延迟;
  10. 再全量开放。

兼容要求:

  • 旧用户无需迁移密码;
  • password_hash IS NULL 的用户继续正常验证码登录;
  • 旧客户端无需感知新接口;
  • 不修改 /auth/login 的请求和响应;
  • 不用默认密码批量填充历史账号。

24. 回滚

发生问题时:

  1. 设置 PASSWORD_LOGIN_ENABLED=false;
  2. 保留数据库中的 BCrypt 摘要,不清空用户密码;
  3. 验证码登录继续工作;
  4. 停止前端展示密码入口;
  5. 不回滚已成功执行的 Flyway;
  6. 根据固定审计事件调查,不导出密码或摘要;
  7. 修复后重新执行全量与黑盒验收再开启。

关闭密码入口不能恢复已撤销的旧会话;用户可通过验证码重新登录。

25. 预期修改范围

实施阶段预计涉及:

  • auth/ 与 auth/password/ 生产代码;
  • controller/AuthController.java;
  • config/SecurityConfig.java;
  • common/exception/ErrorCode.java 与 ErrorHttpStatusRegistry.java;
  • application.yml 和测试 profile;
  • 一条新的 Flyway(仅在批准 password_changed_at 后);
  • OpenAPI、认证文档和前端联调文档;
  • 密码专项测试、认证回归和真实 MySQL/Redis 验收脚本。

禁止顺带修改:

  • S02 Token 格式、密钥、有效期和轮换策略;
  • 用户角色与资源归属;
  • 评分、排名、推荐、邀约、申诉;
  • 简历上传、隐私和查重;
  • 消息发送与撤回;
  • 已执行的 V1–V45;
  • 本机号码一键登录或第三方登录。

26. 实施门禁确认

负责人于 2026-09-20 确认以下三项门禁:

  1. 批准新增 users.password_changed_at;
  2. 接受密码登录及密码敏感操作在 Redis 不可用时失败关闭;
  3. 批准新增密码专用 ErrorCode,并把 CREDENTIALS_INVALID(2005) 显式映射为 HTTP 401。

本文设计据此进入“已确认、可实施”状态。实现仍须从届时实际主线分配 Flyway 版本号和新增错误码数字,不得修改既有迁移或占用已被其他并行模块使用的编号。

其余产品与安全决策已在本文固定。任何偏离必须先更新设计并记录原因,不能在实现过程中隐式改变。