7.8 KiB
设备与用户设备关系(方案定稿)
不改代码前的设计说明。与 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
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
CREATE TABLE t_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)
CREATE TABLE t_user_devices (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '行 id,表内自增',
user_id BIGINT NOT NULL COMMENT '用户 uid',
device_id BIGINT UNSIGNED NOT NULL COMMENT 't_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
ALTER TABLE t_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与 JWTaid一致。
Login 响应
仍返回 device_id、user_id;可增 app_id 与 JWT 一致(可选,避免客户端只解 JWT)。
登录流程(POST /v1/auth/login)
- 校验 Body
app_id、OTP 等。 - email 查/建用户。
- 指纹 →
device_id→ UPSERTt_devices。 - UPSERT
t_user_devices(user_id, device_id)。 - INSERT
t_user_login_logs(含当次app_id)。 Issue(user_id, device_id, app_id)(Bodyapp_id→ JWTaid;PairStore 按pair_key_mode)。
TCP AUTH
- 验 access token →
user_id+device_id+app_id。 - 连接绑定
(user_id, device_id)(与现网一致);app_id进 conn 上下文 供日志/限流扩展。 - Body
device_id可选,须与 JWTdid一致;不要求 Body 再传app_id(以 tokenaid为准)。
关系小结
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。