3.4 KiB
Context
用户已有 status(启停)、lockedAt/lockReason(锁定)与 tokenVersion;登录在 loginTenant 内校验,checkUserActive 每请求经 Redis user-state 缓存校验。PATCH /auth/me/password(changePassword)已支持改本人密码(校验原密码、tokenVersion+1、删 refresh)。动机见 proposal.md - Why。
Goals / Non-Goals
Goals:
- 临时密码(管理员创建/重置、建租户随机密码)强制首登改密,且后端硬门禁不可绕过。
Non-Goals:
- 密码复杂度/历史/过期策略(另立 change)。
- 手机/邮箱找回密码流程。
Decisions
D1 — must_change_password 布尔列
User 新增 must_change_password(Boolean,非空,默认 false)。现有用户无需回填(默认 false,即不强制)。
D2 — 硬门禁内置于 JwtAuthGuard
user-state 缓存对象扩展为 { status, tokenVersion, lockedAt, mustChangePassword };checkUserActive 改为返回 { active, mustChangePassword }(单次 Redis 读)。守卫在 active 通过后,若 mustChangePassword 且请求非白名单 → 403「请先修改初始密码」。
- 白名单仅
PATCH /auth/me/password(改本人密码)。login/refresh/logout为@Public不经过守卫天然放行;登出依赖 refresh token、无需门禁。 - 备选:独立
MustChangePasswordGuard(新 APP_GUARD)——否决,多一个全局守卫且需与 JwtAuthGuard 共享 user-state,不如内聚到一处。
D3 — 触发置标记
置 mustChangePassword=true:
users.createUser:CreateUserDto新增可选mustChangePassword(默认 true,管理员可显式关闭);users.resetPassword(管理员重置,新临时密码,恒强制);tenants.createTenant(随机密码),经initTenant新增forcePasswordChange选项传入(恒强制)。
不置(false):
auth.register(用户自选密码);seed的平台自举管理员(touyun/Touyun@123,开发与 e2e/smoke 依赖其可直接登录)。
D4 — 登录返回标志、改密清除
loginTenant 返回 mustChangePassword(直接读已取到的 user 行),自动探测登录路径同步透传;changePassword 成功时 must_change_password: false。
D5 — 前端强制流程 + 状态持久化
auth store 新增 mustChangePassword,登录后写入并持久化(sessionStorage,抗刷新);路由守卫:待改密且目标非改密页/登出时重定向改密页。改密页为独立页(不进布局,不依赖 menus/permissions),提交成功后 reset() 清会话并跳登录。
- 为什么独立页:硬门禁会 403 menus/permissions,若改密页在布局内会被门禁卡住。
Risks / Trade-offs
- [门禁误伤] → 白名单最小化到仅
PATCH /auth/me/password;@Public接口天然绕过不受影响。 - [seed 管理员不强制] → 仅开发自举账号豁免;生产建租户/建用户/重置密码均强制,安全闭环不缺失。
- [前端刷新丢状态] → 后端硬门禁兜底(即便前端标志丢失,其余接口仍 403);前端仍持久化标志以提供干净重定向体验。
Migration Plan
- Prisma migration:
sys_user加must_change_password BOOLEAN NOT NULL DEFAULT false(无回填)。 - 代码上线(守卫门禁 + 登录标志 + 触发点)。
- 回滚:向后兼容,回滚代码即可。
Open Questions
无。