feat(dcauth): 新增后台手机号与设备认证登录

登录链路改为账号密码 + 短信验证码 + DCAuth 设备签名,并新增后台手机号绑定、
登录日志、设备绑定与重置接口。
This commit is contained in:
yml2213
2026-10-21 20:46:24 +08:00
parent 7542d547bf
commit 040100f290
36 changed files with 3128 additions and 41 deletions
+142
View File
@@ -0,0 +1,142 @@
# dcauth 设备认证协议 v1
三个系统共用同一个 Windows 客户端 `DCAuth.exe`。客户端通过自定义协议被浏览器唤起,使用本机 Ed25519 私钥对服务端 challenge 签名,实现“账号 + 密码 + 短信验证码 + 本机设备”登录。
## 1. 适用范围
| app | 系统 | 生产入口 |
|---|---|---|
| `order_site` | 订单后台 | `https://order.khhao.com` |
| `live_hub` | 直播运营后台 | `https://live.khhao.com` |
| `affiliate_dash` | 皮肤分销后台 | `https://skin.khhao.com` |
本地开发入口由客户端内置白名单放行,生产环境只允许上表 HTTPS 入口。
## 2. 客户端协议
```text
dcauth://authorize?app=<app>&challenge_id=<id>&challenge=<base64url>&api=<urlencoded-api-origin>
```
客户端收到协议后:
1. 校验 `app` 是否为已知应用。
2. 校验 `api` 的 origin 是否在该应用白名单内。
3. 弹出确认框(显示应用名和“确认本机登录”)。
4. 读取本机设备私钥,生成签名。
5. 向 `${api}/api/dcauth/authorize` 提交签名。
6. 浏览器轮询 challenge 状态,拿到一次性 ticket 后完成登录。
客户端不允许打开任意 URL,也不接受任意 `api`,防止被恶意网页利用。
## 3. 设备身份
- 私钥:Ed25519,保存在 `%LOCALAPPDATA%\DCAuth\device.key`,使用 Windows DPAPI 加密。
- `deviceId`:`D1-` + Base32(SHA256(`dcauth.device.v1` + 公钥)) 前 20 位。
- `machineCode`:`M1-` + Base32(SHA256(硬件指纹)) 前 20 位。
- 硬件指纹采集:主板 UUID、Windows MachineGuid、主板序列号、BIOS 序列号、系统盘序列号、CPU ProcessorId。
- 不采集 MAC、IP、主机名作为主要依据;空值、全 0、`To be filled by O.E.M.` 等占位值会被丢弃。
- 服务端只保存机器码哈希/脱敏值和设备公钥,不保存原始硬件序列号。
## 4. 签名原文
使用 UTF-8、换行分隔,字段顺序固定:
```text
dcauth.v1
<app>
<challenge_id>
<challenge>
<device_id>
<machine_code>
<timestamp>
```
`timestamp` 为 Unix 秒。服务端允许 ±300 秒偏差。
## 5. 后端接口
所有系统统一使用以下路径:
### 创建 challenge
```http
POST /api/dcauth/challenge
Content-Type: application/json
{"app":"order_site","purpose":"login"}
```
响应:
```json
{
"challengeId": "uuid",
"challenge": "base64url-random",
"expiresAt": "2026-10-21T10:00:00Z",
"pollToken": "一次性轮询密钥"
}
```
### 客户端提交签名
```http
POST /api/dcauth/authorize
Content-Type: application/json
{
"app": "order_site",
"challengeId": "uuid",
"challenge": "base64url-random",
"deviceId": "D1-....",
"machineCode": "M1-....",
"publicKey": "base64url-ed25519-public-key",
"signature": "base64url-ed25519-signature",
"timestamp": 1760000000,
"clientVersion": "1.0.0"
}
```
服务端校验 challenge、app、时间戳、Ed25519 签名后,将 challenge 标记为 `authorized`,并记录 `deviceId/publicKey/machineCode`。
### 浏览器轮询
```http
GET /api/dcauth/challenge/{challengeId}/status
X-Poll-Token: <pollToken>
```
状态:`pending` / `authorized` / `ticket_issued` / `consumed` / `expired` / `rejected`。
`authorized` 时返回一次性 `deviceTicket`,浏览器用它在登录完成接口换取系统会话。
### 登录完成
各系统在自己的登录接口中增加 `deviceTicket` 参数。服务端必须:
1. 验证账号/密码。
2. 验证短信验证码。
3. 消费 `deviceTicket`。
4. 如果用户没有有效设备绑定,则把本次 `deviceId/publicKey/machineCode` 绑定到用户。
5. 如果已有有效绑定,则要求 `deviceId` 和公钥完全一致。
6. 成功后才签发 JWT / Session。
## 6. 首次绑定与重置
- 首次登录自动绑定:账号/密码 + 短信验证码 + 设备签名全部通过后,自动写入有效绑定。
- 一个账号只能有一条有效设备绑定。
- 管理员可以重置绑定;重置后旧会话必须失效(JWT/session version 递增)。
- 同一台机器可以绑定多个账号,但一个账号只能绑定一台设备。
## 7. 兼容策略
- 保留原有账号密码登录接口作为过渡。
- 账号未绑定手机号时,允许旧密码登录,并提示管理员绑定手机号。
- 账号已绑定手机号后,登录必须走“密码 + 短信 + 设备”流程。
- 新账号默认要求绑定手机号;管理员可以在用户管理中设置/修改手机号。
- 管理员可以给技术测试账号打开 `test_account`:该账号允许仅账号密码登录,跳过短信和 DCAuth 设备验证,登录日志记录为 `password_test`。
- 最终可以通过环境变量强制所有账号走新流程。
## 8. 用户名规则
三个系统均允许中文、英文、数字、`.`、`_`、`-`,长度 2–32(affiliate_dash 保留原 3–64 的兼容范围)。不允许空格和控制字符。