Files
Genarrative/docs/technical/PASSWORD_ENTRY_FLOW_DESIGN_2026-04-21.md
2026-05-14 14:21:17 +08:00

268 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 密码登录入口历史落地设计
> 2026-04-25 更新:当前产品策略已调整为“不开放密码注册”。新用户必须通过手机号验证码注册/登录,密码登录只面向已经登录后设置过密码的手机号账号。`POST /api/auth/entry` 只接受 `phone + password`,不支持邮箱、用户名或陶泥号登录,也不承担自动建号能力。本文原有“密码自动建号”内容仅作为历史背景保留,当前落地以本更新和 [PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md](./PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md) 为准。
>
> 2026-04-28 更新:为开发期本地/测试服联调新增服务端环境变量 `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED`,默认 `false`。仅当该变量显式为 `true` 时,`POST /api/auth/entry` 可对未知手机号用本次密码直接创建账号并登录;默认关闭时仍严格保持未知手机号返回 `401` 的生产语义。该开关不得用于生产环境,也不新增任何前端规则说明文案。
日期:`2026-04-21`
## 1. 文档目的
这份文档用于指导 `M2` 中以下两条任务的首版落地:
1. `实现手机号密码登录`
2. `移除密码登录自动注册 / 自动建号语义`
目标是把 `/api/auth/entry` 在 Rust 工作区冻结为手机号验证码账号的补充登录方式:
1. `api-server` 对外只暴露 `phone + password` 的最小接口。
2. `module-auth` 只负责已存在手机号账号的密码校验。
3. 密码入口不创建账号,不接收邮箱、用户名或陶泥号。
4. 登录成功后与 JWT、refresh cookie 的衔接方式。
## 1.1 当前冻结结论
1. 密码登录不是注册入口。
2. 密码登录是手机号验证码登录的补充方式。
3. 只有已存在、已绑定手机号、并已设置密码的账号可以通过密码登录。
4. 未知手机号、未设置密码、密码错误统一返回 `401 UNAUTHORIZED`,避免通过密码入口探测账号状态。
5. 手机号验证码登录仍是新用户注册/首次登录的唯一入口。
## 2. 历史基线
当前 Node `/api/auth/entry` 主链已经具备如下语义:
1. 输入 `username + password`
2. 若用户名不存在,则自动创建一个本地账号。
3. 若用户名已存在,则校验密码。
4. 登录成功后签发 access token。
5. 同时创建 refresh session并把原始 refresh token 写入 HttpOnly cookie。
6. 并发创建同一用户名时,后到的请求会回退为“查已存在账号并校验密码”,不因唯一键冲突直接失败。
这条链路曾经是前端匿名/游客恢复的基础。2026-04-25 起该历史语义已废弃Rust 当前实现必须以“手机号账号已设置密码后登录”为准,不再兼容密码自动建号。
## 3. 设计输入
本任务直接受以下文档约束:
1. [SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md)
2. [SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md)
3. [OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md](./OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md)
4. [PLATFORM_AUTH_JWT_ADAPTER_DESIGN_2026-04-21.md](./PLATFORM_AUTH_JWT_ADAPTER_DESIGN_2026-04-21.md)
5. [PLATFORM_AUTH_REFRESH_COOKIE_ADAPTER_DESIGN_2026-04-21.md](./PLATFORM_AUTH_REFRESH_COOKIE_ADAPTER_DESIGN_2026-04-21.md)
当前冻结点:
1. `password_hash` 当前继续由 `user_account` 承担,不进入 `auth_identity`
2. `sub` 必须是稳定 `user_id`
3. 登录成功后必须继续同时生成 access token 和 refresh session。
4. 密码登录不再保留自动建号兼容,旧开发游客自动建号链路必须迁出 `/api/auth/entry`
## 4. 首版落地范围
本阶段只落以下内容:
1. `module-auth` 中的密码登录用例。
2. `api-server` 中的 `POST /api/auth/entry`
3. 手机号归一化、密码哈希校验与未设置密码拒绝。
4. 登录成功后的 access token 与 refresh cookie 主链打通。
本阶段明确不包含:
1. SpacetimeDB 真正的 `user_account` / `refresh_session` reducer 写入。
2. `/api/auth/me``/api/auth/logout``/api/auth/refresh` 的正式业务闭环。
3. 新增邮箱登录或独立密码注册链路。
## 5. crate 边界
### 5.1 `module-auth`
负责:
1. 手机号与密码的领域校验。
2. 密码登录主用例。
3. 已存在手机号账号与已设置密码约束。
4. 输出登录成功所需的最小用户快照。
不负责:
1. JWT 编解码。
2. refresh cookie 解析与写回。
3. HTTP 请求解析与响应拼装。
### 5.2 `platform-auth`
负责:
1. 密码哈希与校验适配。
2. JWT 签发与校验。
3. refresh cookie 读写适配。
不负责:
1. 决定账号是否应当自动创建。
2. 决定用户状态是否合法。
### 5.3 `api-server`
负责:
1. 解析 `POST /api/auth/entry` 请求体。
2. 调用 `module-auth` 用例。
3. 调用 `platform-auth` 签发 token 和 refresh cookie。
4. 返回与旧接口兼容的 JSON body。
## 6. 请求与响应 contract
### 6.1 请求体
当前 contract
```json
{
"phone": "13800138000",
"password": "secret123"
}
```
### 6.2 成功响应
固定沿用当前 contract
```json
{
"token": "<access-token>",
"user": {
"id": "user_xxx",
"username": "phone_xxx",
"displayName": "138****8000",
"phoneNumberMasked": "138****8000",
"loginMethod": "password",
"bindingStatus": "active",
"wechatBound": false
}
}
```
同时响应头必须写回 refresh cookie。
## 7. 手机号与密码规则
当前阶段固定:
1. `phone` 只接受中国大陆手机号,服务端统一归一化为 `E.164` 后查询。
2. `password` 长度必须在 `6``128` 位之间。
任一校验失败时:
1. 返回 `400 BAD_REQUEST`
2. 错误文案继续保持中文
## 8. 登录校验规则
### 8.1 未知手机号
`phone` 归一化后找不到账号时:
1. 返回 `401 UNAUTHORIZED`
2. 不创建账号。
3. 不写 `password_hash`
开发期例外:
1.`GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=true` 时,未知手机号会创建手机号账号。
2. 新账号立即写入本次密码的 `password_hash`,并将 `password_login_enabled` 置为 `true`
3. 成功响应沿用密码登录响应体,`created` 只保留在领域结果中,不额外暴露到当前 HTTP contract。
4. 手机号格式和密码长度校验仍完全沿用正式密码入口规则。
### 8.2 未设置密码
当账号存在但 `password_login_enabled = false` 时:
1. 返回 `401 UNAUTHORIZED`
2. 不区分“未设置密码”和“密码错误”的外部文案。
### 8.3 已设置密码
当账号存在且已设置密码时:
1. 校验密码哈希。
2. 校验失败返回 `401 UNAUTHORIZED`
3. 校验成功签发 access token 与 refresh cookie。
## 9. 首版存储策略
当前阶段为了先跑通工程闭环,固定采用:
1. `module-auth` 内的进程内内存仓储适配器作为临时真相。
说明:
1. 这是阶段性工程策略,不改变最终 `SpacetimeDB` 作为真相源的目标。
2. 当前这样做是为了先把 crate 边界、用例形状、HTTP contract、JWT / refresh cookie 主链稳定下来。
3. 后续切到 `SpacetimeDB` 时,应保持 `module-auth` 用例接口不变,只替换仓储实现。
## 10. 密码哈希策略
当前阶段继续对齐 Node
1. `Argon2id`
说明:
1. Rust 侧不再复用 Node 原生库,但哈希语义继续保持同类算法。
2. 当前目标是“工程能力闭环”,不是做跨语言哈希值兼容迁移。
3. 若未来需要与 Node 历史哈希共存,需单独补兼容文档和迁移策略。
## 11. 与 JWT / refresh cookie 的衔接
密码登录成功后:
1. `module-auth` 返回最小用户领域对象。
2. `api-server` 基于该对象构造 `AccessTokenClaimsInput`
3. `platform-auth` 签发 access token。
4. `platform-auth` 生成 refresh token 与 `Set-Cookie` 头。
5. `api-server` 返回 `token + user`
当前阶段固定 claims 值:
1. `provider = password`
2. `roles = ["user"]`
3. `binding_status = active`
4. `phone_verified = false`
5. `display_name = username`
## 12. 测试策略
当前阶段至少覆盖:
1. 未知手机号密码登录返回 `401`,且不创建账号。
2. 已登录手机号账号设置密码后可用 `phone + password` 登录。
3. 同手机号错误密码返回 `401`
4. 邮箱、用户名或陶泥号作为密码登录标识返回 `400`
5. 登录成功时返回 access token。
6. 登录成功时写回 refresh cookie。
7. `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED` 默认关闭时行为不变。
8. 开关开启时,未知手机号可通过 `/api/auth/entry` 创建账号并登录;同手机号后续用相同密码登录复用同一用户,错误密码仍返回 `401`
## 13. 完成定义
满足以下条件时,本任务视为完成:
1. `module-auth` 不再只是 README占位被真实 crate 实现替换。
2. `POST /api/auth/entry` 可在 Rust 侧独立跑通。
3. 密码入口不注册、不接收邮箱/用户名的行为可验证。
4. JWT 与 refresh cookie 登录成功主链打通。
5. 文档、任务清单与测试同步完成。
## 14. 后续衔接
这条任务完成后,下一步顺序固定为:
1. `me` 查询
2. refresh token 轮换
3. 会话吊销
4. 手机验证码登录
微信登录继续按“暂缓执行”处理,直到用户重新解锁。