spark-server/docs/devices.md

210 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 设备与用户设备关系(方案定稿)
不改代码前的设计说明。与 JWT、`user_devices`、`user_login_logs`、PairStore 对齐。
## 目标
| 项 | 规则 |
| --- | --- |
| 物理设备 | 同一指纹 → **一个 `device_id`**(指纹 **不含 `app_id`**),多 App 同机共用 |
| **用户** | **同一邮箱 = 同一 `user_id`**,与从哪个 App 登录无关 |
| **App 会话** | Login Body **`app_id` → JWT `aid`**;**`user_login_logs.app_id`** 记当次 App |
| **JWT** | **`did` + `aid`(app_id)** |
| **PairStore** | 默认 **`(user_id, device_id, app_id)`**;配置可切 **`(user_id, device_id)`**(见 §PairStore) |
| **`users`** | **不增加 `app_id` 列**;同一邮箱同一用户 |
| **`user_devices`** | 仅 **`(user_id, device_id)`** + 登录/活跃时间,**不存 `app_id`** |
| **审计** | **`user_login_logs.app_id`**:当次从哪个 App 登录 |
| API | **`GET /v1/users/me/devices` 首版不提供** |
在线 / 活跃:更新 **`user_devices(user_id, device_id)`**;**当前 App** 看 JWT **`aid`** / 登录日志,不看 `user_devices`。
---
## 枚举
### `device_type`
| value | name |
| ---: | --- |
| 0 | unknown |
| 1 | ios |
| 2 | android |
| 3 | harmony |
| 4 | windows |
| 5 | macos |
| 6 | web |
### `app_id`
| value | name |
| ---: | --- |
| 1 | spark_im |
| … | … |
---
## 指纹与 `device_id`
```text
payload = "{device_type}:{idfv|android_id}"
fingerprint_hash = HEX(SHA256(payload))
device_id = uint64(SHA256(fingerprint_hash)[0:8]),非 0
```
- **`devices`**:`UNIQUE(fingerprint_hash)`,**无 `app_id` 列**。
- Android 多 App 共 **`device_id`**:客户端集团统一上报策略(见前版说明),服务端不合并不同 ANDROID_ID。
---
## 表:`users`
**不新增 `app_id`**。登录仍 **仅按 email** 查/建用户(`user_emails` 不变,同一邮箱 = 同一 `user_id`)。
---
## 表:`devices`
```sql
CREATE TABLE devices (
device_id BIGINT UNSIGNED NOT NULL COMMENT 'JWT did;多 App 同机共用',
fingerprint_hash CHAR(64) NOT NULL COMMENT '物理指纹,UNIQUE,不含 app_id',
device_type TINYINT UNSIGNED NOT NULL COMMENT 'device_type 枚举',
device_name VARCHAR(64) NOT NULL DEFAULT '' COMMENT '最近 login 上报设备名',
idfv VARCHAR(36) NOT NULL DEFAULT '',
android_id VARCHAR(20) NOT NULL DEFAULT '',
last_client_ip VARCHAR(30) NOT NULL DEFAULT '' COMMENT '最近 RealIP',
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (device_id),
UNIQUE KEY uk_fingerprint (fingerprint_hash)
) COMMENT='物理设备;与用户、App 无关';
```
---
## 表:`user_devices`(无 `app_id`)
```sql
CREATE TABLE user_devices (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '行 id,表内自增',
user_id BIGINT NOT NULL COMMENT '用户 uid',
device_id BIGINT UNSIGNED NOT NULL COMMENT 'devices.device_id',
last_login_at DATETIME(3) NOT NULL COMMENT '该用户在此设备最近登录成功',
last_active_at DATETIME(3) NULL COMMENT '该用户在此设备最近活跃',
created_at DATETIME(3) NOT NULL COMMENT '该用户首次在此设备绑定',
PRIMARY KEY (id),
UNIQUE KEY uk_user_device (user_id, device_id),
KEY idx_user_id (user_id),
KEY idx_device_id (device_id)
) COMMENT='用户与设备关系;当前 App 见 JWT aid';
```
| 场景 | 说明 |
| --- | --- |
| 同机用户 A、B | `(A,did)`、`(B,did)`,**同一 `devices` 行** |
| **同邮箱、App1 与 App2 登录** | **同一 `user_id`**;**同一 `device_id`** → **一行 `user_devices`**;JWT **`aid` 不同**(各次 login) |
| B 在用、查 A 设备活跃 | 只更新 **A 的 `(A,did)`**,与 B 无关 |
---
## 表:`user_login_logs`
```sql
ALTER TABLE user_login_logs
ADD COLUMN app_id TINYINT UNSIGNED NOT NULL DEFAULT 0
COMMENT '当次登录 App,见 meta app_id' AFTER device_id;
```
当次 **`app_id`** = Login Body(当次从哪个 App 登录)。
---
## JWT
### Claims(access / refresh 均含)
| claim | JSON | 说明 |
| --- | --- | --- |
| `sub` | uid | 不变 |
| **`did`** | device_id | 服务端指纹生成 |
| **`aid`** | app_id | **当次登录 App**(Body `app_id`),refresh 链上固定 |
| `use` | access / refresh | 不变 |
| `jti` / `exp` / … | | 不变 |
### PairStore(refresh 键策略)
配置项(示例 **`auth.pair_key_mode`**):**`triple`** | **`pair`**,**上线前可改**,实现须 **两种都支持**。
| 模式 | Redis / 内存键 | **App2 登录同 `(user, device)` 时** |
| --- | --- | --- |
| **`triple`(默认)** | `(user_id, device_id, app_id)` | **不踢** App1 的 refresh;App1 / App2 **各一套** access+refresh |
| **`pair`** | `(user_id, device_id)` | **踢掉**该用户在该设备上 **所有 App** 的旧 refresh(及对应 access denylist),只保留 **最新一次 Issue** |
**JWT 始终带 `aid`**,与模式无关;Authenticate / refresh 仍校验 token 内 **`aid`**。
**`triple` → Issue`**:只 **replace** 当前 **`(uid, did, aid)`** 这一对 jti。
**`pair` → Issue`**:`replacePair` 前 **Delete 该 `(uid, did)` 下全部 pair 键**(或 scan 前缀 `auth:pair:{uid}:{did}:*`),再写入新 pair;可选 **LogoutAllAppsOnDevice** 钩子踢 TCP 仅 **`(uid, did)`**(不按 aid 拆连接,与现 TCP 模型一致)。
**Refresh**:键解析与 Issue 同模式;refresh token 内 **`aid` 必须与存储键一致**。
**Logout(当前设备)**:
- `triple`:只删 **`(uid, did, aid)`**(JWT 当前 `aid`)。
- `pair`:删 **`(uid, did)`** 唯一 pair(等价于该设备上所有 App session 一并注销)。
**迁移建议**:先 **`triple`** 上线;切 **`pair`** 前公告「同设备换 App 登录将踢其它 App 登录态」;Redis 键规范预留 **`auth:pair:{uid}:{did}`** 与 **`auth:pair:{uid}:{did}:{aid}`** 两种 layout,或统一 hash field 由 mode 决定 field 名。
### 签发与校验(其它)
- **`Issue(userID, deviceID, appID)`**。
- **Authenticate**:**`did` + `aid`** → Principal / reqctx。
- 可选 Header **`X-App-ID`** 与 JWT **`aid`** 一致。
### Login 响应
仍返回 **`device_id`**、**`user_id`**;可增 **`app_id`** 与 JWT 一致(可选,避免客户端只解 JWT)。
---
## 登录流程(`POST /v1/auth/login`)
1. 校验 Body **`app_id`**、OTP 等。
2. **email** 查/建用户。
3. 指纹 → **`device_id`** → UPSERT **`devices`**。
4. UPSERT **`user_devices(user_id, device_id)`**。
5. INSERT **`user_login_logs`**(含当次 **`app_id`**)。
6. **`Issue(user_id, device_id, app_id)`**(Body `app_id` → JWT `aid`;PairStore 按 **`pair_key_mode`**)。
---
## TCP AUTH
- 验 access token → **`user_id` + `device_id` + `app_id`**。
- 连接绑定 **`(user_id, device_id)`**(与现网一致);**`app_id` 进 conn 上下文** 供日志/限流扩展。
- Body **`device_id`** 可选,须与 JWT **`did`** 一致;**不要求 Body 再传 `app_id`**(以 token **`aid`** 为准)。
---
## 关系小结
```text
JWT aid ← 当次登录 App
JWT did ← devices.device_id
user_devices (user_id, device_id)
user_login_logs.app_id
PairStore ← triple 或 pair(配置)
```
---
## 迁移与检查清单
- [ ] **`users` 不加 `app_id`**;`user_emails` 不变
- [ ] `devices` / `user_devices` / `user_login_logs.app_id`
- [ ] JWT **`aid`** + Principal / reqctx
- [ ] **`auth.pair_key_mode`**:`triple`(App2 **不踢** App1 refresh)/ `pair`(**踢**全 App 同设备 refresh)
- [ ] PairStore 实现 **双模式** + Redis 键 layout 可切换
- [ ] login OpenAPI:`app_id` 必填;去掉客户端 **`device_id` 必填**
- [ ] meta **`app_id`** 枚举
首版不做:`GET /v1/users/me/devices`、attestation。