迁移后端认证与拆分 Spacetime 客户端
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# Auth refresh session 持久化热修方案
|
||||
|
||||
日期:`2026-04-24`
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前 Rust 鉴权链路已经具备 refresh cookie 自动续签能力:access token 过期后,前端会调用 `POST /api/auth/refresh`,后端轮换 refresh token 并返回新的 access token。
|
||||
|
||||
但 `module-auth` 当前仍使用进程内 `InMemoryAuthStore` 保存账号与 refresh session。只要 `server-rs` 在 access token 生命周期内发生重启,浏览器侧 HttpOnly cookie 仍然存在,服务端却找不到对应账号与 session,最终表现为约 `JWT_EXPIRES_IN` 后需要重新登录。
|
||||
|
||||
## 2. 本次目标
|
||||
|
||||
本次先落一个低风险持久化闭环,解决“后端重启导致 2 小时后必须重新登录”的线上体验问题:
|
||||
|
||||
1. 为当前 `InMemoryAuthStore` 增加 UTF-8 JSON 快照文件。
|
||||
2. 在账号、手机号索引、微信身份、refresh session 发生变更后自动保存快照。
|
||||
3. `api-server` 启动时从配置路径恢复快照。
|
||||
4. 保持现有 `/api/auth/refresh`、`logout`、`sessions` 语义不变。
|
||||
|
||||
## 3. 非目标
|
||||
|
||||
本次不把完整认证域一次性迁入 SpacetimeDB 表,原因是 refresh session 独立持久化不足以解决问题:refresh 成功后还需要按 `user_id` 读取账号快照重新签发 access token,因此账号主数据也必须同源恢复。
|
||||
|
||||
SpacetimeDB 正式表接管仍按以下既有文档继续推进:
|
||||
|
||||
1. `docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md`
|
||||
2. `docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md`
|
||||
3. `docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md`
|
||||
|
||||
## 4. 配置
|
||||
|
||||
新增环境变量:
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GENARRATIVE_AUTH_STORE_PATH` | `server-rs/.data/auth-store.json` | 当前 Rust 鉴权快照文件路径。相对路径按进程工作目录解析。 |
|
||||
|
||||
## 5. 数据边界
|
||||
|
||||
快照文件保存当前 Rust 鉴权服务已经在内存中维护的最小真相:
|
||||
|
||||
1. `next_user_id`
|
||||
2. `users_by_username`
|
||||
3. `phone_to_user_id`
|
||||
4. `sessions_by_id`
|
||||
5. `session_id_by_refresh_token_hash`
|
||||
6. `wechat_identity_by_provider_uid`
|
||||
7. `user_id_by_provider_union_id`
|
||||
|
||||
短信验证码和微信 OAuth state 不持久化,原因是它们是短生命周期挑战数据,重启后失效是可接受行为。
|
||||
|
||||
## 6. 安全约束
|
||||
|
||||
1. refresh token 原文仍只存在浏览器 HttpOnly cookie,快照只保存 `sha256(refresh_token)`。
|
||||
2. 快照包含 `password_hash`、手机号映射和 refresh token hash,部署时必须放在服务端私有目录,不允许暴露到静态资源目录。
|
||||
3. 快照写入必须使用 UTF-8,并通过临时文件原子替换降低写坏风险。
|
||||
|
||||
## 7. 后续 SpacetimeDB 接管点
|
||||
|
||||
当 `user_account`、`auth_identity`、`refresh_session` 表及 reducer 全部落地后,替换策略如下:
|
||||
|
||||
1. 保留 `module-auth` 用例语义。
|
||||
2. 把当前快照仓储替换为 SpacetimeDB 仓储适配器。
|
||||
3. 启动时可提供一次性导入脚本,把 JSON 快照导入 SpacetimeDB 表。
|
||||
4. 导入完成后禁用 `GENARRATIVE_AUTH_STORE_PATH` 快照写入。
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Auth SpacetimeDB 快照迁移 Stage 1
|
||||
|
||||
日期:`2026-04-24`
|
||||
|
||||
## 1. 背景
|
||||
|
||||
`AUTH_REFRESH_SESSION_PERSISTENCE_HOTFIX_2026-04-24.md` 已把 Rust 鉴权内存态落到本地 JSON 快照,解决 `server-rs` 重启后 refresh session 丢失导致 `JWT_EXPIRES_IN` 到期后必须重新登录的问题。
|
||||
|
||||
本阶段继续把该快照迁入 SpacetimeDB,作为正式 `user_account/auth_identity/refresh_session` 表完全拆分前的过渡真相源。
|
||||
|
||||
## 2. 阶段目标
|
||||
|
||||
1. 在 `spacetime-module` 新增私有 `auth_store_snapshot` 表。
|
||||
2. 表内只保存一条当前 Axum 鉴权快照 JSON,主键固定为 `default`。
|
||||
3. 新增 `get_auth_store_snapshot` 与 `upsert_auth_store_snapshot` procedure,供 `api-server` 同步读写。
|
||||
4. `module-auth` 继续拥有鉴权业务语义,SpacetimeDB 只承接当前阶段的持久化真相。
|
||||
|
||||
## 3. 为什么先做快照表
|
||||
|
||||
只迁 `refresh_session` 表无法恢复登录态,因为 refresh 成功后仍必须按 `user_id` 找到账号快照重新签发 access token。因此正式拆表必须同时完成账号、身份、会话三组 reducer。
|
||||
|
||||
本阶段先把已经验证过的 JSON 快照从本地文件迁到 SpacetimeDB,收益是:
|
||||
|
||||
1. 后端进程重启后可恢复登录态。
|
||||
2. 多实例部署时可共享同一份鉴权快照。
|
||||
3. 后续正式拆表时有统一导入来源。
|
||||
|
||||
## 4. 表设计
|
||||
|
||||
表名:`auth_store_snapshot`
|
||||
|
||||
访问级别:private table
|
||||
|
||||
字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `snapshot_id` | `String` | 主键,固定为 `default`。 |
|
||||
| `snapshot_json` | `String` | `module-auth` 当前持久化快照 JSON。 |
|
||||
| `updated_at` | `Timestamp` | 最近写入时间。 |
|
||||
|
||||
## 5. Procedure 设计
|
||||
|
||||
### 5.1 `get_auth_store_snapshot`
|
||||
|
||||
输入:无。
|
||||
|
||||
输出:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"snapshotJson": "...",
|
||||
"updatedAtMicros": 123456789,
|
||||
"errorMessage": null
|
||||
}
|
||||
```
|
||||
|
||||
当记录不存在时,`snapshotJson = null`。
|
||||
|
||||
### 5.2 `upsert_auth_store_snapshot`
|
||||
|
||||
输入:
|
||||
|
||||
```json
|
||||
{
|
||||
"snapshotJson": "...",
|
||||
"updatedAtMicros": 123456789
|
||||
}
|
||||
```
|
||||
|
||||
输出同 `get_auth_store_snapshot`。
|
||||
|
||||
## 6. 后续拆表迁移点
|
||||
|
||||
Stage 2 再把 `auth_store_snapshot.snapshot_json` 导入并拆分为:
|
||||
|
||||
1. `user_account`
|
||||
2. `auth_identity`
|
||||
3. `refresh_session`
|
||||
|
||||
拆分完成后,`auth_store_snapshot` 只保留为迁移备份,不再作为运行时写入目标。
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# 密码登录与自动建号落地设计
|
||||
# 密码登录历史落地设计
|
||||
|
||||
> 2026-04-24 更新:当前产品策略已调整为“不开放密码注册”。新用户必须通过手机号验证码注册/登录,密码登录只面向已经设置密码的账号。密码修改与重置以 [PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md](./PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md) 为准;本文中“密码自动建号”仅保留为历史基线说明,不再作为当前落地依据。
|
||||
|
||||
日期:`2026-04-21`
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# 密码登录、修改与重置落地设计
|
||||
|
||||
日期:`2026-04-24`
|
||||
|
||||
## 1. 目标边界
|
||||
|
||||
本次迭代开放账号密码登录、登录后修改密码、手机号验证码重置密码,但不开放密码注册。
|
||||
|
||||
1. 新用户只能通过手机号验证码完成注册与首次登录。
|
||||
2. 已有用户可以在登录后设置或修改密码。
|
||||
3. 忘记密码时,只能通过已绑定手机号验证码重置密码。
|
||||
4. 密码登录只校验已存在且已设置密码的账号,不自动创建新账号。
|
||||
|
||||
## 2. 接口设计
|
||||
|
||||
### 2.1 密码登录
|
||||
|
||||
沿用现有 `POST /api/auth/entry`:
|
||||
|
||||
1. 请求字段:`username`、`password`。
|
||||
2. 用户不存在时返回 `401`,不创建账号。
|
||||
3. 用户存在但未设置密码时返回 `401`。
|
||||
4. 校验成功后签发 access token,并写入 refresh cookie。
|
||||
|
||||
### 2.2 修改密码
|
||||
|
||||
新增 `POST /api/auth/password/change`:
|
||||
|
||||
1. 需要 Bearer 登录态。
|
||||
2. 请求字段:`currentPassword`、`newPassword`。
|
||||
3. 若账号未设置过密码,允许 `currentPassword` 为空并设置首个密码。
|
||||
4. 若账号已有密码,必须校验 `currentPassword` 后才能写入 `newPassword`。
|
||||
5. 修改成功后递增用户 `token_version`,使旧 access token 失效;前端沿用当前 refresh 会话刷新登录态。
|
||||
|
||||
### 2.3 重置密码
|
||||
|
||||
新增 `POST /api/auth/password/reset`:
|
||||
|
||||
1. 不需要 Bearer 登录态。
|
||||
2. 请求字段:`phone`、`code`、`newPassword`。
|
||||
3. 使用 `reset_password` 短信场景校验验证码。
|
||||
4. 手机号不存在时返回 `404`,避免用密码重置隐式注册账号。
|
||||
5. 重置成功后签发新的 access token,并写入 refresh cookie,便于用户直接进入登录态。
|
||||
|
||||
### 2.4 发送重置验证码
|
||||
|
||||
复用 `POST /api/auth/phone/send-code`,`scene` 增加 `reset_password`。
|
||||
|
||||
## 3. 前端交互
|
||||
|
||||
登录弹窗拆成两个页签:
|
||||
|
||||
1. `登录`:提供密码登录、手机号验证码登录、忘记密码入口。
|
||||
2. `注册`:只提供手机号验证码注册/登录,不提供账号密码注册。
|
||||
3. `忘记密码`:从登录页进入独立重置面板,不在当前表单下方展开。
|
||||
4. 账号设置面板提供密码修改入口;未设置密码的账号显示为设置密码。
|
||||
|
||||
## 4. 数据约束
|
||||
|
||||
进程内认证快照中的 `password_hash` 改为可空语义:
|
||||
|
||||
1. 手机号新建账号默认没有用户可用密码。
|
||||
2. 微信待绑定账号默认没有用户可用密码。
|
||||
3. 只有用户显式修改或重置密码后,才允许密码登录。
|
||||
|
||||
后续迁移到 SpacetimeDB 表时,保持同一语义:密码哈希字段允许为空,密码登录 reducer 不承担注册能力。
|
||||
@@ -190,6 +190,8 @@ session snapshot 中的 `resultPreview` 固定输出:
|
||||
7. 最终兜底 `还在收集你的世界锚点。`
|
||||
8. `subtitle` 先取 `draft_profile_json.subtitle`
|
||||
9. 否则用 `stageLabel`
|
||||
10. 同一 `source_agent_session_id` 同时存在未发布 Agent 会话草稿与 `custom_world_profile` 草稿时,works 只输出一条草稿;优先保留可继续聊天的 `agent_session`,避免作品库把“聊天中的草稿”和“待发布草稿 profile”展示成两份作品。
|
||||
11. 只有找不到同源未发布 Agent 会话,或 profile 已经发布时,`custom_world_profile` 才作为独立作品输出。
|
||||
|
||||
### 4.4 已发布 works 最小取值规则
|
||||
|
||||
|
||||
Reference in New Issue
Block a user