feat(dcauth): 新增后台手机号与设备认证登录
登录链路改为账号密码 + 短信验证码 + DCAuth 设备签名,并新增后台手机号绑定、 登录日志、设备绑定与重置接口。
This commit is contained in:
@@ -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 的兼容范围)。不允许空格和控制字符。
|
||||
Reference in New Issue
Block a user