268 lines
9.3 KiB
Markdown
268 lines
9.3 KiB
Markdown
# 密码登录入口历史落地设计
|
||
|
||
> 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. 手机验证码登录
|
||
|
||
微信登录继续按“暂缓执行”处理,直到用户重新解锁。
|