210 lines
7.8 KiB
Markdown
210 lines
7.8 KiB
Markdown
# 设备与用户设备关系(方案定稿)
|
||
|
||
不改代码前的设计说明。与 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。
|