# 手机号密码认证设计 > 文档类型:设计说明与实现参考 > 状态:设计已确认,可进入实施;尚未编码 > 适用项目:`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` | | 用户状态 | `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 密码登录 ```http POST /auth/password/login Content-Type: application/json ``` ```json { "phone": "13800138000", "password": "correct horse battery staple" } ``` 成功时返回与现有验证码登录相同的用户与 Token 字段: ```json { "code": 200, "message": "success", "data": { "userId": 123, "phone": "13800138000", "nickname": "用户8000", "role": "STUDENT", "accessToken": "", "refreshToken": "", "expiresIn": 86400 }, "requestId": "..." } ``` 约束: - `phone` 必须为 11 位数字; - `password` 必填,输入上限 64 个字符; - 不存在的手机号不得创建用户; - 失败原因统一,不返回“用户不存在”“尚未设置密码”“账号停用”等差异信息; - 成功后清除该手机号引用的失败计数,但不清除 IP 窗口计数。 ### 5.2 首次设置密码 ```http POST /auth/password/set Authorization: Bearer Content-Type: application/json ``` ```json { "smsCode": "123456", "newPassword": "correct horse battery staple" } ``` 前置步骤: ```http POST /auth/sms/send?phone=<当前登录手机号>&purpose=password-set ``` 规则: - 手机号只能来自 Access Token 对应的当前用户,客户端不得传入手机号; - 验证码用途必须严格为 `password-set`; - 仅 `password_hash IS NULL` 时允许; - 已有密码返回状态冲突,不得覆盖; - 成功后撤销全部会话,调用方当前 Access Token 随即失效; - 成功响应不签发新 Token。 成功响应: ```json { "code": 200, "message": "success", "data": { "passwordConfigured": true, "reauthenticationRequired": true }, "requestId": "..." } ``` ### 5.3 修改密码 ```http POST /auth/password/change Authorization: Bearer Content-Type: application/json ``` ```json { "currentPassword": "old password", "newPassword": "new correct horse battery staple" } ``` 规则: - 只接受 Access Token,Refresh Token 必须返回 401; - 当前密码错误统一返回凭据错误; - 未设置密码时返回密码状态冲突,引导用户走首次设置; - 新旧密码不得相同; - 成功后撤销全部会话且不签发新 Token。 ### 5.4 找回并重置密码 前置步骤: ```http POST /auth/sms/send?phone=13800138000&purpose=password-reset ``` 重置: ```http POST /auth/password/reset Content-Type: application/json ``` ```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 弱密码名单 第一版使用仓库内只读资源,例如: ```text src/main/resources/security/common-passwords.txt ``` 该文件: - 只包含公开常见弱密码,不包含任何用户数据; - 在启动时加载为不可变集合; - 加载失败时应用启动失败,不能静默关闭策略; - 后续更新必须作为可审查的版本化变更; - 本轮不调用外部泄漏密码 API,避免把用户密码或派生值发送给第三方。 ## 8. BCrypt 存储与升级 ### 8.1 写入 ```text normalized = NFC(rawPassword) password_hash = BCryptPasswordEncoder.encode(normalized) ``` 禁止: - 明文密码落库; - SHA/MD5 等快速摘要; - 在数据库增加明文盐; - 把密码、摘要或其前缀写入日志、审计、响应、消息或异常。 ### 8.2 校验 ```text 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 密钥生成不可逆引用: ```text phoneRef = HMAC-SHA256(PASSWORD_RATE_KEY_SECRET, normalizedPhone) ipRef = HMAC-SHA256(PASSWORD_RATE_KEY_SECRET, normalizedIp) ``` 只使用截断后的十六进制引用作为 key 片段;HMAC 原始值不得进入日志。 ### 9.2 Redis key ```text 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 HTTP/1.1 401 Unauthorized ``` ```json { "code": 2005, "message": "手机号或密码错误", "requestId": "..." } ``` 实现要求: - 不存在用户仍执行 dummy BCrypt; - inactive 等状态不得在校验前快速返回; - 响应状态、业务码、正文结构和主要计算路径一致; - 不承诺网络层严格恒定耗时,但独立测试需验证不存在明显快速分支; - 找回密码的发送入口统一返回“如账号可用,验证码将发送”。 ## 11. 状态机 ```text 未设置密码 ├─ 有效 Access + password-set 验证码 ─→ 已设置密码 └─ password-reset 验证码 ────────────→ 已设置密码 已设置密码 ├─ 手机号 + 正确密码 ────────────────→ 创建登录会话 ├─ 当前密码 + 新密码 ────────────────→ 已修改,全部会话撤销 ├─ password-reset 验证码 ────────────→ 已重置,全部会话撤销 └─ 再次 set ─────────────────────────→ 409 状态冲突 inactive / 注销完成 └─ 任何密码操作 ─────────────────────→ 统一拒绝,不复活账号 ``` ## 12. 事务与会话边界 ### 12.1 密码登录 ```text Controller(无事务) → 检查 Redis IP/手机号限制 → PasswordAuthenticationService.login(REQUIRED) → 查询用户 → BCrypt 真/假摘要校验 → 校验用户可用性与注销状态 → AuthSessionService.createSession → 写成功审计 → 事务提交后清理手机号失败计数 ``` 会话记录保存失败时,登录事务回滚且不得返回 Token。 ### 12.2 设置、修改、重置 每个操作必须在单个 REQUIRED 事务内完成: ```text 锁定用户行/进行带版本条件更新 → 验证当前身份或原密码/短信验证码 → 校验新密码策略 → 写 password_hash(及 password_changed_at,如批准) → revokeAllUserSessions(reason=...) → 写脱敏审计 → 提交 ``` 任一步失败必须整体回滚。特别是: - 验证码不能已消费而密码未更新; - 密码不能已更新而旧会话仍活动; - 审计失败时是否回滚沿用现有 `AuditService` 的事务行为; - 成功响应只可在事务提交后返回。 ### 12.3 会话撤销原因 新增白名单原因: ```text PASSWORD_SET PASSWORD_CHANGED PASSWORD_RESET ``` 不得把手机号、密码、验证码或操作来源拼入 `revocation_reason`。 ## 13. 并发与幂等 | 场景 | 预期 | |---|---| | 两次并发首次设置 | 恰好一次成功,另一次 409;最终只有一个摘要 | | 两次并发修改 | 使用数据库行锁或乐观版本保证旧密码只成功一次 | | 两次并发重置 | 同一验证码只能消费一次;恰好一次成功 | | 修改密码与 Refresh 并发 | 密码事务提交后的终态必须无活动旧会话 | | 登录与密码修改并发 | 密码修改提交后不得留下使用旧密码创建的活动会话 | | 重复成功请求 | 不得重复写审计、重复创建会话或返回同一 Token | 仅调用现有 `revokeAllUserSessions` 不足以自动证明“登录/刷新与密码变更并发后无活动会话”。实现阶段必须设计数据库锁顺序或安全版本栅栏,并用真实 MySQL 并发测试证明终态。禁止使用 `synchronized`、JVM 锁或 `Thread.sleep` 作为正确性机制。 推荐实现:密码变更事务对 `users` 行加写锁;登录和 Refresh 在创建后继会话前以同一锁顺序读取可用用户。若评估发现会扩大 S02 锁竞争,必须先补独立事务设计,不得直接上线。 ## 14. 数据模型 ### 14.1 现有字段 继续使用: ```sql users.password_hash VARCHAR(255) NULL ``` `NULL` 表示尚未设置密码,不代表账号不可用;用户仍可使用验证码登录。 ### 14.2 建议新增字段 建议新增: ```sql 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: ```text 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 匿名放行: ```text POST /auth/password/login POST /auth/password/reset ``` 需要有效 Access Token: ```text POST /auth/password/set POST /auth/password/change ``` 规则: - 不能使用宽泛的 `/auth/** permitAll`; - Refresh Token 访问 set/change 必须为 401; - STUDENT、COMPANY、ADMIN 均可管理自己的密码; - 角色不参与密码认证成功与否,只进入 Token 的既有 role claim; - 不接受 `userId`、`sessionId` 或角色作为请求参数。 ## 18. 配置 建议配置: ```yaml 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. 组件建议 建议新增或调整: ```text 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 版本号和新增错误码数字,不得修改既有迁移或占用已被其他并行模块使用的编号。 其余产品与安全决策已在本文固定。任何偏离必须先更新设计并记录原因,不能在实现过程中隐式改变。