26 KiB
手机号密码认证设计
文档类型:设计说明与实现参考 状态:设计已确认,可进入实施;尚未编码 适用项目:
mydreamjob-server基线:master/dedd47e3db42fee26bbeb21f1925ca1da127fb65设计日期:2026-09-20
1. 目标
在不破坏现有手机号验证码登录的前提下,增加手机号密码认证能力:
- 手机号密码登录;
- 首次设置密码;
- 使用旧密码修改密码;
- 使用短信验证码找回并重置密码;
- 密码错误计数、临时锁定和手机号/IP 双维度限流;
- 密码变更后撤销该用户全部登录会话;
- 防止账号枚举、时序侧信道以及日志、审计和响应泄漏。
所有登录方式认证成功后必须复用现有 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 接受规则
- NFC 规范化后长度为 12–64 个 Unicode code point;
- 允许空格和 Unicode 字符;
- 不自动删除首尾空格;
- 不自动转换大小写;
- 不能等于或包含完整手机号;
- 不能命中项目内置、版本化的弱密码集合;
- 新密码不能与当前密码相同。
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 与升级
- 默认沿用当前
BCryptPasswordEncoderstrength 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,至少覆盖:
- Flyway 执行与 Hibernate validate;
- 验证码旧用户设置密码并重新登录;
- 密码登录成功及 Access/Refresh 类型矩阵;
- 不存在、无密码、错误、inactive、注销的统一响应;
- 5 次失败与 15 分钟锁定;
- 单 IP 跨手机号限流;
- Redis 停止后失败关闭,恢复后正常;
- 设置、修改、重置后三类旧会话全部失效;
- 并发设置、重置、登录/修改、Refresh/修改;
- 数据库搜索密码原文和 Token 原文命中 0;
- 日志、审计和响应敏感扫描命中 0,扫描器需注入探针证明有效;
- 关键业务表逐字段副作用快照;
- RabbitMQ ready/unacked 保持 0;
- 静态与运行时 OpenAPI 路径和 operation 集合一致;
- 全量
mvn clean verify为 Failures=0、Errors=0、Skipped=0。
22. Apifox 与前端联调
前端登录页展示:
- 验证码登录;
- 密码登录;
- 忘记密码;
- 已验证码登录用户的“设置密码”;
- 已设置密码用户的“修改密码”。
前端不得根据不同错误文案推断账号状态。密码登录失败统一显示“手机号或密码错误”;限流显示通用等待提示。设置、修改或重置成功后:
- 清除本地 Access/Refresh Token;
- 跳转登录页;
- 提示“密码已更新,请重新登录”。
Apifox 环境继续使用统一 baseUrl,密码登录成功后复用现有后置脚本保存 Access/Refresh Token。
23. 发布和兼容
建议顺序:
- 完成设计评审;
- 从合并时主线确定实际 Flyway 编号;
- 先部署数据库兼容迁移;
- 部署后端但保持
PASSWORD_LOGIN_ENABLED=false; - 验证验证码登录和旧客户端不受影响;
- 配置 Jenkins Credentials 与限流参数;
- 小范围启用密码入口;
- 完成 Apifox 与前端联调;
- 观察登录成功率、限流率、Redis 和 BCrypt 延迟;
- 再全量开放。
兼容要求:
- 旧用户无需迁移密码;
password_hash IS NULL的用户继续正常验证码登录;- 旧客户端无需感知新接口;
- 不修改
/auth/login的请求和响应; - 不用默认密码批量填充历史账号。
24. 回滚
发生问题时:
- 设置
PASSWORD_LOGIN_ENABLED=false; - 保留数据库中的 BCrypt 摘要,不清空用户密码;
- 验证码登录继续工作;
- 停止前端展示密码入口;
- 不回滚已成功执行的 Flyway;
- 根据固定审计事件调查,不导出密码或摘要;
- 修复后重新执行全量与黑盒验收再开启。
关闭密码入口不能恢复已撤销的旧会话;用户可通过验证码重新登录。
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 确认以下三项门禁:
- 批准新增
users.password_changed_at; - 接受密码登录及密码敏感操作在 Redis 不可用时失败关闭;
- 批准新增密码专用 ErrorCode,并把
CREDENTIALS_INVALID(2005)显式映射为 HTTP 401。
本文设计据此进入“已确认、可实施”状态。实现仍须从届时实际主线分配 Flyway 版本号和新增错误码数字,不得修改既有迁移或占用已被其他并行模块使用的编号。
其余产品与安全决策已在本文固定。任何偏离必须先更新设计并记录原因,不能在实现过程中隐式改变。