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