merge: 合并 main 到 feat/cost-center-wip(同步抖音券、设备认证与图标等最新改动)

This commit is contained in:
yml2213
2026-10-22 12:40:18 +08:00
49 changed files with 4257 additions and 40 deletions
+139
View File
@@ -0,0 +1,139 @@
# dcauth 设备认证协议 v2
三个系统共用同一个 Windows 客户端 `DCAuth.exe`。客户端通过自定义协议被浏览器唤起,使用本机 Ed25519 私钥对服务端 challenge 签名,实现“账号 + 密码 + 短信验证码 + 本机设备”登录。
## 1. 适用范围
| app | 系统 | 生产入口 |
|---|---|---|
| `order_site` | 订单后台 | `https://ks.khhao.com`(兼容历史域名 `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 位。
- 设备码只由公钥派生,不包含硬件指纹;换主板、换硬盘或重装系统不会改变设备码。
- 服务端只保存设备公钥和设备码,不采集或保存硬件序列号。
## 4. 签名原文
使用 UTF-8、换行分隔,字段顺序固定:
```text
dcauth.v2
<app>
<challenge_id>
<challenge>
<device_id>
<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-....",
"publicKey": "base64url-ed25519-public-key",
"signature": "base64url-ed25519-signature",
"timestamp": 1760000000,
"clientVersion": "2.0.0"
}
```
服务端校验 challenge、app、时间戳、Ed25519 签名后,将 challenge 标记为 `authorized`,并记录 `deviceId/publicKey`。低于 v2.0.0 的客户端会被要求升级。
### 浏览器轮询
```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` 绑定到用户。
5. 如果已有有效绑定,则要求 `deviceId` 和公钥完全一致。
6. 成功后才签发 JWT / Session。
## 6. 首次绑定与重置
- 首次登录自动绑定:账号/密码 + 短信验证码 + 设备签名全部通过后,自动写入有效绑定。
- 一个账号只能有一条有效设备绑定。
- 管理员可以重置绑定;重置后旧会话必须失效(JWT/session version 递增)。
- 同一台机器可以绑定多个账号,但一个账号只能绑定一台设备。
## 7. 兼容策略
- DCAuth v2 不再包含机器码;v1 客户端会被服务端要求升级。
- 保留原有账号密码登录接口作为过渡。
- 账号未绑定手机号时,允许旧密码登录,并提示管理员绑定手机号。
- 账号已绑定手机号后,登录必须走“密码 + 短信 + 设备”流程。
- 新账号默认要求绑定手机号;管理员可以在用户管理中设置/修改手机号。
- 管理员可以给技术测试账号打开 `test_account`:该账号允许仅账号密码登录,跳过短信和 DCAuth 设备验证,登录日志记录为 `password_test`。
- 最终可以通过环境变量强制所有账号走新流程。
## 8. 用户名规则
三个系统均允许中文、英文、数字、`.`、`_`、`-`,长度 2–32(affiliate_dash 保留原 3–64 的兼容范围)。不允许空格和控制字符。