fix: restrict password login to existing phone accounts

This commit is contained in:
2026-04-26 01:11:45 +08:00
parent c4b9b8173f
commit 0a0f3f1bd8
33 changed files with 489 additions and 778 deletions

View File

@@ -1,6 +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-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-21`
@@ -8,17 +8,25 @@
这份文档用于指导 `M2` 中以下两条任务的首版落地:
1. `实现密码登录`
2. `实现账号自动创建 / 幂等登录兼容策略`
1. `实现手机号密码登录`
2. `移除密码登录自动注册 / 自动建号语义`
目标是先把当前 Node 已经稳定运行的 `/api/auth/entry` 语义迁到 Rust 工作区,并冻结:
目标是 `/api/auth/entry` Rust 工作区冻结为手机号验证码账号的补充登录方式
1. `api-server` 对外暴露的最小兼容接口。
2. `module-auth` 负责的密码登录用例边界
3. 自动建号与并发幂等兼容规则
1. `api-server` 对外暴露 `phone + password` 的最小接口。
2. `module-auth` 负责已存在手机号账号的密码校验
3. 密码入口不创建账号,不接收邮箱、用户名或叙世号
4. 登录成功后与 JWT、refresh cookie 的衔接方式。
## 2. 当前基线
## 1.1 当前冻结结论
1. 密码登录不是注册入口。
2. 密码登录是手机号验证码登录的补充方式。
3. 只有已存在、已绑定手机号、并已设置密码的账号可以通过密码登录。
4. 未知手机号、未设置密码、密码错误统一返回 `401 UNAUTHORIZED`,避免通过密码入口探测账号状态。
5. 手机号验证码登录仍是新用户注册/首次登录的唯一入口。
## 2. 历史基线
当前 Node `/api/auth/entry` 主链已经具备如下语义:
@@ -29,7 +37,7 @@
5. 同时创建 refresh session并把原始 refresh token 写入 HttpOnly cookie。
6. 并发创建同一用户名时,后到的请求会回退为“查已存在账号并校验密码”,不因唯一键冲突直接失败。
这条链路既是当前前端匿名/游客恢复的基础,也是真实 `/api/auth/entry` contract 的既有事实,因此 Rust 首版必须兼容
这条链路曾经是前端匿名/游客恢复的基础。2026-04-25 起该历史语义已废弃Rust 当前实现必须以“手机号账号已设置密码后登录”为准,不再兼容密码自动建号
## 3. 设计输入
@@ -41,12 +49,12 @@
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. 自动建号兼容必须保留,不能因为迁到 Rust 就删除
4. 密码登录不再保留自动建号兼容,旧开发游客自动建号链路必须迁出 `/api/auth/entry`
## 4. 首版落地范围
@@ -54,14 +62,14 @@
1. `module-auth` 中的密码登录用例。
2. `api-server` 中的 `POST /api/auth/entry`
3. 用户名校验、密码哈希校验与自动建号
3. 手机号归一化、密码哈希校验与未设置密码拒绝
4. 登录成功后的 access token 与 refresh cookie 主链打通。
本阶段明确不包含:
1. SpacetimeDB 真正的 `user_account` / `refresh_session` reducer 写入。
2. `/api/auth/me``/api/auth/logout``/api/auth/refresh` 的正式业务闭环。
3. 手机验证码与微信登录链路。
3. 新增邮箱登录或独立密码注册链路。
## 5. crate 边界
@@ -69,9 +77,9 @@
负责:
1. 用户名与密码的领域校验。
1. 手机号与密码的领域校验。
2. 密码登录主用例。
3. 自动建号与并发幂等兼容策略
3. 已存在手机号账号与已设置密码约束
4. 输出登录成功所需的最小用户快照。
不负责:
@@ -106,11 +114,11 @@
### 6.1 请求体
固定沿用当前 contract
当前 contract
```json
{
"username": "guest_001",
"phone": "13800138000",
"password": "secret123"
}
```
@@ -124,9 +132,9 @@
"token": "<access-token>",
"user": {
"id": "user_xxx",
"username": "guest_001",
"displayName": "guest_001",
"phoneNumberMasked": null,
"username": "phone_xxx",
"displayName": "138****8000",
"phoneNumberMasked": "138****8000",
"loginMethod": "password",
"bindingStatus": "active",
"wechatBound": false
@@ -136,11 +144,11 @@
同时响应头必须写回 refresh cookie。
## 7. 用户名与密码规则
## 7. 手机号与密码规则
当前阶段继续对齐 Node 基线
当前阶段固定
1. `username` 只允许 `3``24` 位字母、数字、下划线
1. `phone` 只接受中国大陆手机号,服务端统一归一化为 `E.164` 后查询
2. `password` 长度必须在 `6``128` 位之间。
任一校验失败时:
@@ -148,37 +156,30 @@
1. 返回 `400 BAD_REQUEST`
2. 错误文案继续保持中文
## 8. 自动建号与幂等兼容
## 8. 登录校验规则
### 8.1 自动建
### 8.1 未知手机
`username` 不存在时:
`phone` 归一化后找不到账号时:
1. 用当前请求里的 `password` 生成密码哈希
2. 创建一条本地账号。
3. `display_name = username`
4. `login_provider = password`
5. `account_status = active`
6. `token_version = 1`
1. 返回 `401 UNAUTHORIZED`
2. 创建账号。
3. 不写 `password_hash`
### 8.2 已存在账号
### 8.2 未设置密码
`username` 已存在时:
账号存在但 `password_login_enabled = false` 时:
1. 返回 `401 UNAUTHORIZED`
2. 不区分“未设置密码”和“密码错误”的外部文案。
### 8.3 已设置密码
当账号存在且已设置密码时:
1. 校验密码哈希。
2. 校验失败返回 `401 UNAUTHORIZED`
3. 校验成功继续登录
### 8.3 并发幂等兼容
若两个请求并发创建同一用户名:
1. 允许其中一个请求先创建成功。
2. 后一个请求若命中唯一键冲突,不直接失败。
3. 后一个请求必须重新查询该用户名。
4. 若查到账号,则按“已存在账号”路径继续校验密码。
这保证了当前前端重复调用 `/api/auth/entry` 时可以恢复同一账号,而不是随机失败。
3. 校验成功签发 access token 与 refresh cookie
## 9. 首版存储策略
@@ -226,10 +227,10 @@
当前阶段至少覆盖:
1. 首次密码登录自动建号成功
2. 同用户名同密码可重复登录同一账号
3.用户名不同密码返回 `401`
4. 非法用户名返回 `400`
1. 未知手机号密码登录返回 `401`,且不创建账号
2. 已登录手机号账号设置密码后可用 `phone + password` 登录
3.手机号错误密码返回 `401`
4. 邮箱、用户名或叙世号作为密码登录标识返回 `400`
5. 登录成功时返回 access token。
6. 登录成功时写回 refresh cookie。
@@ -239,7 +240,7 @@
1. `module-auth` 不再只是 README占位被真实 crate 实现替换。
2. `POST /api/auth/entry` 可在 Rust 侧独立跑通。
3. 自动建号与幂等兼容行为可验证。
3. 密码入口不注册、不接收邮箱/用户名的行为可验证。
4. JWT 与 refresh cookie 登录成功主链打通。
5. 文档、任务清单与测试同步完成。