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

227 lines
7.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `/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](./MULTI_DEVICE_SESSION_IDENTITY_DESIGN_2026-04-21.md)
2. [SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md)
3. [AUTH_REFRESH_ROTATION_DESIGN_2026-04-21.md](./AUTH_REFRESH_ROTATION_DESIGN_2026-04-21.md)
4. [AUTH_LOGOUT_CURRENT_SESSION_DESIGN_2026-04-21.md](./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 成功响应
```json
{
"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. `clientRuntime``clientPlatform``deviceDisplayName` 是多端识别首版最小新增字段
6. 小程序来源额外暴露 `miniProgramAppId``miniProgramEnv`
### 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. 派生 `ipMasked``isCurrent`
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. 文档、任务清单与测试已同步更新