227 lines
7.7 KiB
Markdown
227 lines
7.7 KiB
Markdown
# `/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. 文档、任务清单与测试已同步更新
|