Merge pull request 'Implement login recovery flow: admin-issued one-time keys, transaction hardening, browser wiring' (#504) from feature/svc-login-recovery-flow into main
This commit was merged in pull request #504.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,76 @@
|
||||
# 登录恢复流程合同(2026-10-13)
|
||||
|
||||
状态:**已实施**(Dev 任务 `feature/svc-login-recovery-flow`;用户 2026-10-13「按此方案实施」授权)。
|
||||
上游方案:`modules/documents/docs/proposals/2026-10-12-svc-unified-deploy-and-login-key.md`(r2);
|
||||
本文是该方案登录部分的合同定稿与三项裁定记录,U4/U5 规格后续以本文为准回写。
|
||||
|
||||
## 登录初衷(冻结)
|
||||
|
||||
1. 日常使用**用户名 + 密码**登录。
|
||||
2. 忘记密码时,由**管理员签发绑定该用户的一次性登录 Key**(`alk_`)。
|
||||
3. 用户兑换 Key 后进入**受限设密会话**,必须设置新密码;恢复设密**不要求旧密码**。
|
||||
4. 个人设置中的**主动改密仍要求当前密码**。
|
||||
5. 受限会话只开放身份查询、设密、退出;完成前不能进入工作台或访问业务接口。
|
||||
|
||||
## 三项裁定(对 r2 的合同分歧,用户按新方案裁定)
|
||||
|
||||
| 分歧 | r2 表述 | 裁定(本合同) |
|
||||
|---|---|---|
|
||||
| 登录页长期 API 密钥入口 | 保留并标注集成用途 | **移除日常登录入口**;程序化客户端直接带 Authorization 头。浏览器不再持久保存 `ask_` |
|
||||
| 管理员网页恢复方式 | 直接设置新密码(CLI 保留) | 网页改为**签发一次性登录 Key**;旧 `POST /api/admin/users/{owner}/password` 接口兼容保留(调用盘点:仅前端 UsersPanel 使用,已下线) |
|
||||
| must-change 首登会话 | full 会话 + 中间件动态拦截 | **登录即建 `set_password_only` 受限会话**,与恢复会话同形;`me.sessionScope` 统一驱动前端设密门 |
|
||||
|
||||
## 接口合同
|
||||
|
||||
| 接口 | 状态 | 行为 |
|
||||
|---|---|---|
|
||||
| `POST /api/auth/login` | 已有(修订) | 密码验证绑定认证版本;must-change/未设密建受限会话;响应带 `sessionScope` |
|
||||
| `POST /api/auth/login-key/redeem` | 已有 | 请求只有 `{key}`;身份由服务端绑定;按来源 IP 退避(10 次/30s) |
|
||||
| `GET /api/me` | 已有 | 返回 `sessionScope`/`authVia`/`mustChangePassword`/`passwordSet` |
|
||||
| `POST /api/auth/password` | 已有(修订) | 受限会话免当前密码(`CompleteSetPasswordChecked`,事务内复验);主动改密要求当前(单事务) |
|
||||
| `POST /api/auth/logout` | 已有 | 注销当前会话 |
|
||||
| `POST /api/admin/users/{owner}/login-keys` | **新增** | 管理员签发;明文仅响应一次;补发同事务撤销旧 Key 与受限会话 |
|
||||
| `GET /api/admin/users/{owner}/login-keys` | **新增** | 状态/有效期列表,无明文 |
|
||||
| `DELETE /api/admin/users/{owner}/login-keys/{id}` | **新增** | 撤销 Key 并作废该用户受限会话 |
|
||||
|
||||
统一字段:`sessionScope`(`full`|`set_password_only`)、`mustChangePassword`、`passwordSet`、`reloginRequired`。
|
||||
恢复设密成功固定返回登录页(`reloginRequired=true`),用新密码重新登录。
|
||||
|
||||
## 事务与安全要求(对应方案第六节)
|
||||
|
||||
1. 签发不改密码、不吊销正常会话、不动业务数据;补发使旧 Key 与其受限会话立即失效。
|
||||
2. 兑换与建会话原子提交;会话绑定签发时点的 `users.auth_version`。
|
||||
3. 设密事务内复验会话(存在/未过期/scope/版本),统一写密码、清标志、bump 版本、
|
||||
撤 Key、删会话、写审计(`account_events`,v28 迁移)。
|
||||
4. 密码登录会话创建校验认证版本,堵「旧密码验证在途 → 恢复完成 → 旧请求建会话」。
|
||||
5. 主动改密单事务(校验当前密码 → 更新 → 吊销其他会话保留当前)。
|
||||
6. 每次密码写入 bump `auth_version` 并作废全部旧会话;管理员/CLI 重置同事务。
|
||||
7. 会话 cookie 在 TLS(直连或 `X-Forwarded-Proto: https`)下置 `Secure`。
|
||||
8. SSE 长连接:恢复完成后旧会话行已删除,新请求即失效;存量连接的主动断流
|
||||
依赖后续会话失效推送(本合同未实现,遗留项见下)。
|
||||
|
||||
## 验收矩阵与现状
|
||||
|
||||
| 项 | 结果 | 依据 |
|
||||
|---|---|---|
|
||||
| 正常登录/错误凭据 | PASS(SQLite) | `login_recovery_test.go`、既有 e2e |
|
||||
| 只输 Key 识别账户、不能指定身份 | PASS | `TestAdminLoginKeyRecoveryFlow` |
|
||||
| 受限会话拒业务/管理员/签发入口 | PASS | 同上 + 中间件 allowlist |
|
||||
| 100 并发兑换恰好一次成功 | PASS | `TestLoginKeyConcurrentRedeem` |
|
||||
| 补发/撤销使旧 Key 与受限会话失效 | PASS | `TestIssueLoginKeyRevokesDerivedSessions`、`TestAdminLoginKeyReissueAndRevoke` |
|
||||
| 改密事务复验(被撤销会话不能改密) | PASS | `TestCompleteSetPasswordRevalidatesSession` |
|
||||
| 登录竞态(旧版本建会话被拒) | PASS | `TestLoginRaceBlockedByAuthVersion` |
|
||||
| 主动改密缺/错当前密码拒绝 | PASS | `TestActiveChangePasswordRequiresCurrent` |
|
||||
| must-change 首登受限会话 | PASS | `TestLoginMustChangeCreatesRestrictedSession` |
|
||||
| PostgreSQL 双库 | GATE(CI test-pg job) | store 测试走 `openTestStore`,`AGENT_PG_TEST_DSN` 下自动切真库 |
|
||||
| 浏览器 E2E(兑换→设密→重登) | NOT_RUN(本轮) | `e2e/changepw-form.playwright.ts` 已更新门形态;专用兑换 E2E 遗留 |
|
||||
| SSE 存量连接主动断流 | NOT_RUN(遗留) | 见「遗留项」 |
|
||||
|
||||
以上本地结果均为 SELF;GATE 以 PR HEAD 的 CI(含 test-pg)为准。
|
||||
|
||||
## 遗留项(不阻塞本合同)
|
||||
|
||||
- 登录页专用兑换 E2E(login-key.playwright.ts,U5 计划文件)。
|
||||
- SSE 长连接的认证版本主动复验/断流通知。
|
||||
- Documents 主方案 r2 的 U4/U5 规格按本文回写(与 PR #87 合入后再推进,避免文档冲突)。
|
||||
- 兑换入口跨实例限流(当前进程内退避,与 `loginGuard` 同档)。
|
||||
+1
-1
Submodule modules/backend updated: dc736e339d...467718a519
+1
-1
Submodule modules/frontend updated: dcd2699933...9fc7da2ace
Reference in New Issue
Block a user