feat(auth): 首登强制改密(子模块 + openspec change)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b807cc7df8
commit
5685a5ce42
2
openspec/changes/force-password-change/.openspec.yaml
Normal file
2
openspec/changes/force-password-change/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-10-11
|
||||||
57
openspec/changes/force-password-change/design.md
Normal file
57
openspec/changes/force-password-change/design.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
## 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`:
|
||||||
|
1. `users.createUser`:`CreateUserDto` 新增可选 `mustChangePassword`(默认 true,管理员可显式关闭);
|
||||||
|
2. `users.resetPassword`(管理员重置,新临时密码,恒强制);
|
||||||
|
3. `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
|
||||||
|
|
||||||
|
1. Prisma migration:`sys_user` 加 `must_change_password BOOLEAN NOT NULL DEFAULT false`(无回填)。
|
||||||
|
2. 代码上线(守卫门禁 + 登录标志 + 触发点)。
|
||||||
|
3. 回滚:向后兼容,回滚代码即可。
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
无。
|
||||||
29
openspec/changes/force-password-change/proposal.md
Normal file
29
openspec/changes/force-password-change/proposal.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
管理员创建用户、重置密码、建租户下发的都是临时/初始密码,但系统没有「首登强制改密」机制——临时密码可被长期使用,一旦泄漏即越权。企业后台需要一条强制闭环:拿到临时密码 → 首登被拦 → 改密 → 才可用系统。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **待改密标记**:`user` 新增 `must_change_password`(布尔,默认 false);管理员重置密码、`createTenant` 下发随机密码时置 true,管理员创建用户时默认强制(可关闭),用户自行改密成功后清 false。
|
||||||
|
- **登录返回标志**:登录响应新增 `mustChangePassword`。
|
||||||
|
- **后端硬门禁**:未改密用户仅可访问「修改本人密码 / 登出」,其余接口返回 403。
|
||||||
|
- **前端强制流程**:登录后若待改密,强制跳转改密页并拦截其它路由;改密成功后清会话、跳登录重新认证。
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `frontend/AD/force-password-change`: 首登强制改密页面与路由守卫(强制跳转、改密后重新登录)。
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `shared/data-model`: `user` 实体新增 `must_change_password` 待改密标记。
|
||||||
|
- `shared/api-contract`: 登录响应新增 `mustChangePassword` 标志。
|
||||||
|
- `backend/auth`: 登录返回待改密标志、改密清除标志、新增「待改密门禁」。
|
||||||
|
- `frontend/AD/user`: 创建用户表单新增「首登强制改密」配置项。
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **后端**:`auth`(login 返回标志、changePassword 清标志、JwtAuthGuard 加门禁)、`users`(createUser 接受 mustChangePassword 参数、resetPassword 置标记)、`tenants`(createTenant 置标记)、`tenant-template`(initTenant 加 forcePasswordChange 选项);Prisma migration(`sys_user` 加列)。
|
||||||
|
- **前端**:登录 store 保存待改密状态、路由守卫拦截、新增强制改密页、用户创建表单加「首登强制改密」开关、`api/auth.ts` 类型扩展。
|
||||||
|
- **数据**:Prisma migration(`sys_user` 加 `must_change_password`)。
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 待改密门禁
|
||||||
|
系统 SHALL 对 `must_change_password=true` 的用户实施门禁:除「修改本人密码」与「登出」接口外,其余接口返回 403(提示需先修改密码)。
|
||||||
|
|
||||||
|
#### Scenario: 待改密访问受保护接口
|
||||||
|
- **WHEN** 待改密用户请求除改密/登出外的任意接口
|
||||||
|
- **THEN** 系统返回 403 并提示需先修改密码
|
||||||
|
|
||||||
|
#### Scenario: 待改密改密与登出放行
|
||||||
|
- **WHEN** 待改密用户请求修改密码或登出接口
|
||||||
|
- **THEN** 系统放行
|
||||||
|
|
||||||
|
### Requirement: 登录返回待改密标志
|
||||||
|
登录成功 SHALL 在响应中返回 mustChangePassword,值为该用户是否待改密。
|
||||||
|
|
||||||
|
#### Scenario: 待改密登录
|
||||||
|
- **WHEN** 待改密用户登录成功
|
||||||
|
- **THEN** 响应含 mustChangePassword=true
|
||||||
|
|
||||||
|
### Requirement: 改密清除待改密
|
||||||
|
用户修改本人密码成功 SHALL 清除其待改密标记。
|
||||||
|
|
||||||
|
#### Scenario: 改密后清除
|
||||||
|
- **WHEN** 待改密用户修改密码成功
|
||||||
|
- **THEN** 其 must_change_password 置为 false
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
定义首登强制改密页面的行为:登录后对待改密用户强制跳转改密页,改密成功后重新登录。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 强制跳转改密页
|
||||||
|
登录后若 mustChangePassword=true,前端 SHALL 强制跳转改密页并禁止进入其它页面(路由守卫拦截)。
|
||||||
|
|
||||||
|
#### Scenario: 待改密强制跳转
|
||||||
|
- **WHEN** 用户登录成功且 mustChangePassword=true
|
||||||
|
- **THEN** 前端跳转到改密页,访问其它路由被重定向回改密页
|
||||||
|
|
||||||
|
### Requirement: 改密后重新登录
|
||||||
|
改密页 SHALL 提供修改密码表单(新密码);提交成功后清空会话并跳转登录页重新登录。
|
||||||
|
|
||||||
|
#### Scenario: 改密成功
|
||||||
|
- **WHEN** 用户在改密页提交新密码成功
|
||||||
|
- **THEN** 前端清空会话并跳转登录页,用户用新密码重新登录
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 创建用户指定强制改密
|
||||||
|
新增用户表单 SHALL 提供「首登强制改密」配置项(默认勾选);提交时将其作为 mustChangePassword 传入。
|
||||||
|
|
||||||
|
#### Scenario: 默认强制改密
|
||||||
|
- **WHEN** 管理员新增用户保持「首登强制改密」勾选并提交
|
||||||
|
- **THEN** 创建的该用户 mustChangePassword=true
|
||||||
|
|
||||||
|
#### Scenario: 关闭强制改密
|
||||||
|
- **WHEN** 管理员新增用户取消勾选「首登强制改密」
|
||||||
|
- **THEN** 创建的该用户 mustChangePassword=false
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: 认证接口
|
||||||
|
系统 SHALL 提供登录、刷新令牌、登出接口;登录需 tenantCode+username+password,返回 access token、refresh token 与 mustChangePassword(待改密标志)。
|
||||||
|
|
||||||
|
#### Scenario: 登录成功
|
||||||
|
- **WHEN** 用户提供正确的租户、账号与密码
|
||||||
|
- **THEN** 返回 access token(2h 过期)、refresh token(7d 过期)与 mustChangePassword(待改密为 true,否则 false)
|
||||||
|
|
||||||
|
#### Scenario: 刷新令牌
|
||||||
|
- **WHEN** access 过期后客户端携带 refresh token 请求刷新
|
||||||
|
- **THEN** 系统签发新 access 与 refresh,并作废旧 refresh
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 待改密标记
|
||||||
|
user 实体 SHALL 通过 `must_change_password`(布尔,默认 false)标记待改密状态:管理员重置密码、租户初始化下发随机密码、创建用户(默认)时置 true;创建用户时管理员可显式关闭;用户自行改密成功后置 false。
|
||||||
|
|
||||||
|
#### Scenario: 临时密码置标记
|
||||||
|
- **WHEN** 管理员重置密码或初始化租户下发临时密码
|
||||||
|
- **THEN** 该用户 must_change_password 为 true
|
||||||
|
|
||||||
|
#### Scenario: 创建用户默认强制改密
|
||||||
|
- **WHEN** 管理员创建用户(未显式关闭「首登强制改密」)
|
||||||
|
- **THEN** 该用户 must_change_password 为 true;显式关闭则为 false
|
||||||
|
|
||||||
|
#### Scenario: 改密清除标记
|
||||||
|
- **WHEN** 用户修改本人密码成功
|
||||||
|
- **THEN** 该用户 must_change_password 置为 false
|
||||||
31
openspec/changes/force-password-change/tasks.md
Normal file
31
openspec/changes/force-password-change/tasks.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
## 1. 数据模型与迁移
|
||||||
|
|
||||||
|
- [x] 1.1 Prisma:`User` 新增 `mustChangePassword`(Boolean,非空,默认 false,snake_case 映射 `must_change_password`)
|
||||||
|
- [x] 1.2 生成并部署 migration(`migrate dev --create-only` + `migrate deploy`)
|
||||||
|
|
||||||
|
## 2. 认证核心(标志 + 门禁)
|
||||||
|
|
||||||
|
- [x] 2.1 `checkUserActive` 改为返回 `{ active, mustChangePassword }`,user-state 缓存对象加 `mustChangePassword`
|
||||||
|
- [x] 2.2 `JwtAuthGuard` 加待改密门禁:待改密且非 `PATCH /auth/me/password` → 403「请先修改初始密码」
|
||||||
|
- [x] 2.3 `loginTenant` 与自动探测登录响应返回 `mustChangePassword`
|
||||||
|
- [x] 2.4 `changePassword` 成功时清 `must_change_password`
|
||||||
|
|
||||||
|
## 3. 触发置标记
|
||||||
|
|
||||||
|
- [x] 3.1 `CreateUserDto` 新增可选 `mustChangePassword`(默认 true),`users.createUser` 按入参落库
|
||||||
|
- [x] 3.2 `users.resetPassword` 置 `mustChangePassword=true`
|
||||||
|
- [x] 3.3 `initTenant` 新增 `forcePasswordChange` 选项;`createTenant` 传 true、`seed` 传 false
|
||||||
|
|
||||||
|
## 4. 前端强制流程
|
||||||
|
|
||||||
|
- [x] 4.1 `auth` store 新增 `mustChangePassword` 状态并持久化(sessionStorage)
|
||||||
|
- [x] 4.2 `api/auth.ts` 的 `LoginResult` 类型扩展 `mustChangePassword`
|
||||||
|
- [x] 4.3 路由守卫拦截:待改密且目标非改密页/登出时重定向改密页
|
||||||
|
- [x] 4.4 新增独立改密页(不进布局),改密成功后 `reset()` + 跳登录页
|
||||||
|
- [x] 4.5 用户创建表单新增「首登强制改密」开关(默认勾选),`api/user.ts` 的 `UserUpsertData` 扩展 `mustChangePassword` 并在提交时传参
|
||||||
|
|
||||||
|
## 5. 验证
|
||||||
|
|
||||||
|
- [x] 5.1 后端 build + 单测 + e2e 通过
|
||||||
|
- [x] 5.2 前端 type-check 通过
|
||||||
|
- [x] 5.3 冒烟测试:创建用户(待改密)→ 登录返回 mustChangePassword=true → 其它接口 403 → 改密成功 → 重新登录正常
|
||||||
@ -1 +1 @@
|
|||||||
Subproject commit ce45d93ce5526835efb1410caa7fa29fae43edac
|
Subproject commit 5de298a9569027f4b40766814733bea209ccada4
|
||||||
@ -1 +1 @@
|
|||||||
Subproject commit 86832ce32d55310728e00d7e66ca034233e688ec
|
Subproject commit 9869974d4c53a7e1062c80b35d90c7a29d8c632a
|
||||||
Loading…
x
Reference in New Issue
Block a user