Files
Genarrative/docs/technical/AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md

7.7 KiB
Raw Permalink Blame History

/api/auth/sessions 会话列表与多端标识查询设计

日期:2026-04-21

1. 文档目的

这份文档用于指导 M2兼容 /api/auth/sessions 的首版落地,冻结:

  1. GET /api/auth/sessions 的请求与响应 contract
  2. 当前设备识别方式与 isCurrent 语义
  3. 多端登录识别字段如何从 refresh_session 派生到 DTO
  4. Rust 首版在 Axum + 进程内 module-auth 下的最小实现边界
  5. 2026-05-13 会话组合并展示与远端踢下线闭环修复口径

2. 当前基线

当前 Node /api/auth/sessions 已具备以下稳定行为:

  1. 依赖 Bearer JWT 确认用户身份
  2. 从 refresh cookie 识别当前设备
  3. 返回当前账号全部未吊销活跃会话
  4. 每条记录给出端侧标签、最近活跃时间、到期时间、IP 脱敏信息与是否当前设备

当前问题是:

  1. 旧实现只能粗略给出“网页端浏览器 / 移动端浏览器”
  2. 无法稳定区分同设备不同浏览器
  3. 无法区分微信内 H5 与微信小程序、小程序平台来源

因此本次 /api/auth/sessions 首版落地必须直接承接多端会话身份模型。

3. 设计输入

本任务直接受以下文档约束:

  1. MULTI_DEVICE_SESSION_IDENTITY_DESIGN_2026-04-21.md
  2. SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md
  3. AUTH_REFRESH_ROTATION_DESIGN_2026-04-21.md
  4. AUTH_LOGOUT_CURRENT_SESSION_DESIGN_2026-04-21.md

4. 首版落地范围

本阶段只落以下内容:

  1. module-auth 提供按 user_id 读取活跃 refresh session 列表的能力
  2. api-server 暴露 GET /api/auth/sessions
  3. 登录创建 session 时落库结构化客户端身份字段
  4. 会话列表返回多端识别所需字段,并兼容旧 clientLabel

2026-05-13 起,本接口同时承担账号安全页的会话组读模型:

  1. 后端按“同设备 + 同 IP”聚合活跃 refresh_session
  2. 前端只消费后端聚合结果,不自行推断合并
  3. POST /api/auth/sessions/{sessionId}/revoke 已纳入 Rust 实现,用于踢下线非当前会话

本阶段仍明确不包含:

  1. SpacetimeDB reducer / view 正式读表
  2. 登录方式、refresh token 轮换策略或账号安全页整体重设计

5. 请求与响应 contract

5.1 请求

  1. 方法:GET
  2. 路径:/api/auth/sessions
  3. 请求体:空
  4. 鉴权:
    • Bearer JWT 必填
    • refresh cookie 选填但建议携带,用于判断 isCurrent

5.2 成功响应

{
  "sessions": [
    {
      "sessionId": "usess_xxx",
      "sessionIds": ["usess_xxx", "usess_yyy"],
      "sessionCount": 2,
      "clientType": "web_browser",
      "clientRuntime": "chrome",
      "clientPlatform": "windows",
      "clientLabel": "Windows / Chrome",
      "deviceDisplayName": "Windows / Chrome",
      "miniProgramAppId": null,
      "miniProgramEnv": null,
      "userAgent": "Mozilla/5.0 ...",
      "ipMasked": "203.0.*.*",
      "isCurrent": true,
      "createdAt": "2026-04-21T10:00:00Z",
      "lastSeenAt": "2026-04-21T10:05:00Z",
      "expiresAt": "2026-05-21T10:00:00Z"
    }
  ]
}

字段说明:

  1. sessionId 是聚合组代表会话 ID若组内包含当前 sid,代表 ID 必须使用当前会话 ID
  2. sessionIds 是该聚合组内全部活跃 session ID前端批量踢下线时逐个调用 revoke
  3. sessionCount 是聚合组内 session 数量
  4. clientLabel 当前阶段继续兼容旧前端字段,值固定与 deviceDisplayName 保持一致
  5. clientRuntimeclientPlatformdeviceDisplayName 是多端识别首版最小新增字段
  6. 小程序来源额外暴露 miniProgramAppIdminiProgramEnv

5.3 失败响应

以下情况返回 401 UNAUTHORIZED

  1. Bearer JWT 缺失或非法
  2. Bearer JWT 对应用户不存在

仓储读取失败返回 500 INTERNAL_SERVER_ERROR

6. 当前设备识别规则

isCurrent 固定按以下规则判断:

  1. 从 refresh cookie 读取当前原始 refresh token
  2. 在 Axum 侧计算 sha256(refresh_token)
  3. 与会话列表中的 refresh_token_hash 比较
  4. 同时读取 Bearer access token claims 中的 sid
  5. 聚合组内任意 session 命中当前 refresh hash 或当前 sid,则整组 isCurrent = true

说明:

  1. 如果请求没有携带 refresh cookie本接口仍可返回会话列表
  2. 此时仍可通过 Bearer sid 标记当前组
  3. 当前组不允许在前端显示“踢下线”,当前设备退出必须走 /api/auth/logout

6.1 会话组合并规则

同设备同 IP 的 active refresh sessions 在后端合并为一条 DTO

  1. 优先使用 device_fingerprint + ip 作为聚合 key
  2. device_fingerprint 时退化为 client_type + client_runtime + client_platform + device_display_name + user_agent + ip
  3. createdAt 取组内最早 created_at
  4. lastSeenAt 取组内最新 last_seen_at
  5. expiresAt 取组内最新 expires_at
  6. ipMasked 仍只返回脱敏 IP

7. 多端标识派生规则

7.1 后端入库字段

登录创建会话时Axum 必须先解析并写入:

  1. client_type
  2. client_runtime
  3. client_platform
  4. client_instance_id
  5. device_fingerprint
  6. device_display_name
  7. mini_program_app_id
  8. mini_program_env
  9. user_agent
  10. ip

7.2 DTO 派生规则

会话列表返回时:

  1. clientType = client_type
  2. clientRuntime = client_runtime
  3. clientPlatform = client_platform
  4. deviceDisplayName = device_display_name
  5. clientLabel = device_display_name
  6. miniProgramAppId = mini_program_app_id
  7. miniProgramEnv = mini_program_env

8. crate 边界

8.1 module-auth

负责:

  1. 保存 refresh session 客户端身份快照
  2. user_id 返回活跃会话列表
  3. 保持 refresh 轮换后 session_id 稳定、客户端身份字段不漂移

8.2 api-server

负责:

  1. 读取 Bearer JWT 与 refresh cookie
  2. 按同设备同 IP 聚合活跃会话
  3. 把活跃会话组映射成旧接口兼容 DTO
  4. 派生 ipMaskedisCurrent
  5. 暴露 POST /api/auth/sessions/{sessionId}/revoke

8.3 指定会话吊销接口

POST /api/auth/sessions/{sessionId}/revoke 固定规则:

  1. Bearer JWT 必填
  2. 仅允许吊销当前用户自己的非当前会话
  3. 当前会话自吊销返回业务错误,提示使用退出登录
  4. 只撤销目标 refresh_session,不递增 token_version
  5. 撤销后同步 auth store 到 SpacetimeDB
  6. 认证中间件会校验 access token sid 对应 active refresh_session,因此被踢设备已有 access token 会立即失效

9. 测试策略

至少覆盖:

  1. 同一账号在同平台不同浏览器登录后,会话列表能返回两条不同运行时记录
  2. 微信内 H5 登录后,会话列表返回 wechat_h5 + wechat_embedded_browser
  3. 显式小程序头优先于 User-Agent 判断
  4. 请求携带当前 refresh cookie 时,只有当前会话 isCurrent = true
  5. 同设备同 IP 会话会合并,并返回 sessionIds/sessionCount
  6. 合并组包含当前 sid 或当前 refresh hash 时,整组 isCurrent = true
  7. 指定远端会话吊销后,被踢设备 access token 立即无法通过认证

10. 完成定义

满足以下条件时,本任务视为完成:

  1. Rust 侧已提供 GET /api/auth/sessions
  2. 会话列表可区分普通浏览器、微信内 H5、小程序来源
  3. 同设备不同浏览器可在会话列表中清晰区分
  4. clientLabel 与新增多端字段都已稳定返回
  5. 同设备同 IP 的重复 active refresh sessions 已合并展示
  6. 非当前会话可通过真实 revoke 接口踢下线
  7. 文档、任务清单与测试已同步更新