spark-server/docs/devices.md

7.8 KiB
Raw Blame History

设备与用户设备关系(方案定稿)

不改代码前的设计说明。与 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 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 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

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 为准)。

关系小结

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。