Why: The remaining open items (API-key SSE closure, cross-instance redeem backoff, U7 atomic bootstrap/create, CLI credential delivery) landed in Backend #192; Dev pins it and closes the contract leftovers. Evidence: Backend #192 CI success at head bc1e0a3 (GATE); composite CI on this PR re-runs the suite incl. test-pg (GATE). Impact: backend pin cfb8308; contract leftover section now records all items closed; task card H1-H6 requirement table. Refs: Backend #192; docs/svc-deploy/2026-10-13-login-recovery-contract.md.
6.5 KiB
6.5 KiB
登录恢复流程合同(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 规格后续以本文为准回写。
登录初衷(冻结)
- 日常使用用户名 + 密码登录。
- 忘记密码时,由管理员签发绑定该用户的一次性登录 Key(
alk_)。 - 用户兑换 Key 后进入受限设密会话,必须设置新密码;恢复设密不要求旧密码。
- 个人设置中的主动改密仍要求当前密码。
- 受限会话只开放身份查询、设密、退出;完成前不能进入工作台或访问业务接口。
三项裁定(对 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),用新密码重新登录。
事务与安全要求(对应方案第六节)
- 签发不改密码、不吊销正常会话、不动业务数据;补发使旧 Key 与其受限会话立即失效。
- 兑换与建会话原子提交;会话绑定签发时点的
users.auth_version。 - 设密事务内复验会话(存在/未过期/scope/版本),统一写密码、清标志、bump 版本、
撤 Key、删会话、写审计(
account_events,v28 迁移)。 - 密码登录会话创建校验认证版本,堵「旧密码验证在途 → 恢复完成 → 旧请求建会话」。
- 主动改密单事务(校验当前密码 → 更新 → 吊销其他会话保留当前)。
- 每次密码写入 bump
auth_version并作废全部旧会话;管理员/CLI 重置同事务。 - 会话 cookie 在 TLS(直连或
X-Forwarded-Proto: https)下置Secure。 - 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)为准。
遗留项(2026-10-13 全部收口)
登录页专用兑换 E2E:已补齐(Frontend #77,e2e/login-key.playwright.ts; 真实浏览器全流程验收 PASS——签发→只输 Key 兑换→免旧密码设密→新密码登录→重复兑换被拒, 隔离 auth-on 实例,收据 sha256be7ae9a2…/b3a9e691…绑定 Dev HEAD 268ef29,SELF)。SSE 长连接断流:已实现(Backend #189 会话凭证;#192 补齐 API key 凭证)—— 心跳点复验,失效即以session-expired终止(默认 15s,测试可缩短)。Documents 主方案回写:已完成(Documents #94 r3,含三裁定与实施记录)。兑换入口跨实例限流:已实现(Backend #192,迁移 v29redeem_attempts共享库 窗口计数;双 Store 连接同库共担限流的测试覆盖)。U7 余量:原子开户/引导、CLI 凭据安全交付、PG 锁串行首管:已实现 (Backend #192)——CreateAccount 单行 INSERT(并发同名 1 胜且胜者凭据不被覆盖); BootstrapFirstAdmin PG 咨询锁 + SQLite 插入后复查,建号+激活 Key+审计同事务, 已有用户拒绝接管(R11);CLIAGENT_SYSTEM_DSN环境输入与--out0600 文件交付。- 部署与 live 账户验收:本任务执行部署与可验证项;owner 浏览器输密码一步 按合同由用户完成(密码不交给 Agent)。