Files
Commilitia-Drop/auth/docs/子会话方案.md
T
admin a2cad11224 后台/会话三诉求:子会话(一台设备一会话)+ 离线消息队列 + 被登出推送 + 后台唤醒层
- Req 1 子会话(iOS 多进程在用户视角=一台设备一会话,内部多条独立刷新逻辑会话):brokerclient 加 MintParams.Sub + SessionInfo.Sub + RevokeDeviceSessions(user,meta) 级联;device-session sub!="" 挂在主 device_id 之下、走 tier=clipboard 且不建第二个 device 行;handleSessionsList 按 meta 归并、隐藏 sub!=""(但保留“仅控件会话存活”的设备,不简单丢弃);revokeDevice 改走 meta 级联(移除设备连带吊控件子会话)。iOS provisionWidgetSessionIfNeeded 改用主 device_id + sub="widget"(弃用独立 dev_widget),控件令牌仍隔离持有、独立刷新——不碰引擎主会话,#7 隔离不变。蓝本 auth/docs/子会话方案.md(cdrop 提案 + Broker 评审接受 + cdrop 确认 6 点:用 sub、R1 cdrop 归并、scope 复用 tier=clipboard、级联 DELETE …?meta=)。
- Req 2a 离线消息队列:新增 pending_messages 表(0001_init + sqlc 查询);message.go 收件设备离线即入队(无论是否配推送都入队,恒 202),修“离线消息只随推送横幅一闪、不入收件列表”;GET /api/messages/pending 取即删(DELETE..RETURNING,单次投递);web hub.ts onOpen 每次 SSE 连接 / 重连即拉取补收、逐条 addMessage(全端受益,iOS 经桥推原生 + 累积未读);复用 login-request reaper 清 TTL。
- Req 3 被登出推送:push 加 KindSessionRevoked + 本地化文案,apns/web 双通道;revokeDevice 加 notifyRevoked 参(跨端 revoke=true、自登出=false),跨端移除时推送告知被踢设备;iOS AppDelegate 收 session:revoked 即清 Keychain 主会话 + 控件会话、发 .cdropSessionRevoked,前台经 AppRoot 即时回登录页、关闭态下次启动即登出。
- #6 后台唤醒层:apns apsEnvelope 加 content-available:1(服务端有消息时既弹可点横幅又短暂后台唤醒);iOS DeviceItem 加 Codable、presence 快照持久化(冷启即时显示设备列表,缓解“长时间重连”观感,SSE 一连即整组替换自校正);控件会话回前台补铸(scenePhase active)+ content-available 唤醒时刷新隔离的控件会话(剪贴板保活,绝不碰引擎主会话以免 #7 回归)。
- 测试:qr_test mock broker 加 sub 幂等键 + 级联吊销端点;bootstrap_test 期望表集加 pending_messages。
2026-06-27 17:42:53 +08:00

210 lines
18 KiB
Markdown
Raw 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.
# 子会话(subordinate session)— 提交 Auth Broker 评审
> 在现有“委派设备会话”(R1 列举 + R2 幂等铸造,`meta` 为稳定设备身份)之上,引入“子会话”:
> 一台设备下可有多条独立令牌的逻辑会话,但对外仍呈现为**一台设备、一条会话**。
> 状态:**待 Broker 评审**。关联:委派设备会话模型、cdrop `internal/httpapi/device_session.go` / `sessions.go`。
---
## 1. 背景与动机
cdrop 的 iOS 客户端天然是**多进程**:
- **主 app**:完整会话,跑 SSE 信令 / 传输 / presence。
- **控制中心控件 + 主屏小组件扩展**:独立 OS 进程,仅调云剪贴板 REST(`/api/clipboard`)。系统会在主 app 关闭 / 墓碑时单独拉起该扩展,故它必须能**脱离主 app 独立工作**。
这两个进程需要**各自独立的令牌生命周期**:单次轮换(single-use rotating)的 refresh token 若被两进程共用,会并发抢轮换——一方轮换后另一方持有的旧 refresh 立即失效,触发误登出。cdrop 已在主会话上踩过同类坑(冷启并发 401 抢轮换 → 误 forceLogout → 隔夜被迫重登,已修)。结论:**控件会话必须是与主会话隔离的第二条逻辑会话。**
但在**用户视角**,二者必须是**一台设备、一条会话**:
- 在用户**任何设备**的“设备 / 会话”列表里(该列表由 Broker R1 权威呈现,cdrop 仅叠加 type/online),iOS 只能出现**一次**。
- **登出该 iOS 设备**(在别处吊销)应**连带**登出其控件会话——不能留一条游离的控件会话还能读写剪贴板。
当前模型“一个 `meta` = 一台设备 = 一条会话”(R2 同 `meta` 即轮换替换那一条)无法表达“一台设备下多条逻辑会话”:若控件用同 `meta` 铸造,R2 会把主会话的令牌轮换掉(打断主 app);若控件用另一个 `meta`,它就成了**第二台设备**,违反“一个设备”诉求。
---
## 2. 诉求:最小扩展为“子会话”
在一条**父设备会话**之下铸一条**子会话**:
- **共享父的设备身份**(同一 `meta`)→ R1 里仍是同一台设备。
- **独立令牌对**access + refresh),**独立刷新**,与父互不抢轮换。
- 可带**缩减 scope**(如 `clipboard`),最小权限。
- **连带吊销**:吊销设备 / 父会话即吊销其全部子会话。
- 对外(R1 列表、任何 Broker 账户 UI)**不单独呈现**为设备 / 会话。
---
## 3. 数据模型与语义(建议)
### 3.1 会话增加子标识 `role`
- 会话表增可选列 `role`(或 `sub`),默认空串 `""` = 主会话。
- **R2 幂等键**从 `(user, meta)` 扩为 `(user, meta, role)`
- `(user, meta, "")` = 主会话(行为不变,老客户端零影响)。
- `(user, meta, "widget")` = 控件子会话,与主会话**并存**、各自独立令牌。
-`(user, meta, role)` 再铸 = 幂等轮换那一条(沿用现 R2 语义到 role 维度)。
### 3.2 scope 缩减
- 子会话铸造时可声明 `scope`(如 `clipboard`),Broker 在签发的令牌里带上;cdrop 路由层据此只放行剪贴板(cdrop 已有 `requireScope("clipboard")` 同款门,复用)。
### 3.3 列表 R1 归并
- R1 **按 `meta` 归并**:一台 `meta` 只呈现**一条**设备 / 会话(取主会话的 label / last_seen 等)。
- 子会话**不单列**。两种实现皆可,Broker 择一:
- (a) R1 直接隐藏 `role != ""` 的行;或
- (b) R1 仍返回但带 `role` / `parent_sid` 字段,由消费方(cdrop)归并隐藏。
- cdrop 倾向 (a)(权威端归并最干净,任何消费方都见一台)。
### 3.4 吊销级联
- 按**设备 `meta`** 吊销(cdrop 的“移除设备”/“登出”即按 device_id=`meta` 走)→ Broker 级联吊销该 `meta` 下**全部 role**(主 + 子)。
- 按单条 `sid` 吊销沿用现语义(可单独吊销某子会话,可选)。
### 3.5 刷新
- 每条会话按**自身** refresh token 独立轮换,无需区分主 / 子。**无跨进程抢轮换**——这正是隔离两条会话的目的。
---
## 4. API 形态(建议,二选一)
**方案甲(最小侵入)**:现有 device-session 铸造端点加两个可选参数 `role``scope`。缺省 `role=""` 即今日行为。
**方案乙(显式)**:新增 `POST /device/subsession { parent_meta, role, scope }`,语义同上。
刷新 / 撤销端点**无需改签名**:刷新按 refresh token;撤销按 sid 沿用,新增“按 meta 级联撤销”(cdrop 移除设备时用)。
---
## 5. cdrop / iOS 侧配合(Broker 支持后)
- iOS 控件会话改为“**主设备 `meta` + `role=widget` + `scope=clipboard`**”铸造,**弃用**独立 `dev_widget` device_id。
- cdrop `/api/sessions` 直接呈现 Broker 归并后的列表 → iOS 只出现一台。
- 移除设备 / 登出按 `meta` 级联 → 控件会话随之失效。
- 控件令牌泄露面缩到“仅剪贴板”。
改动量小且不触 cdrop 的主会话刷新热路径(不引入 #7 类风险)。
---
## 6. 备选与取舍
- **cdrop 侧过滤(不改 Broker**cdrop 在自己的 `/api/sessions` 里隐藏控件会话行。问题:
- 只修 cdrop 自己的列表视图;Broker **权威 R1** + 任何 Broker 账户 UI / 其他消费方仍见**两条**——违反“在用户任何设备的列表里都是一个”。
- 级联吊销需 cdrop **自行维护**父子映射并双吊销,Broker 不知二者关联,脆弱、易漏。
- 控件会话在 Broker 仍算一台“设备”,占设备数配额、被 Broker 自身管理面看见。
- **结论**:不干净,仅作 Broker 支持前的**临时回退**(若需提前上线,可先 cdrop 侧隐藏 + 按 user 维度级联,待 Broker 子会话就绪再切换)。
---
## 7. 安全要点
- **scope 最小化**:子会话仅 `clipboard`,缩小扩展进程(独立沙箱)令牌泄露面。
- **级联吊销**:保证“登出一台 iOS”彻底,含其全部子会话——不留游离凭证。
- **独立刷新**:各会话自轮换,无跨进程抢单次轮换 refresh(杜绝 #7 类误登出)。
- **老客户端零影响**`role=""` 即现行为,R2/R1/refresh/revoke 对既有会话语义不变。
---
## 8. 给 Broker 的一句话
> 在“委派设备会话”上,把 R2 幂等键由 `(user, meta)` 扩为 `(user, meta, role)`R1 按 `meta` 归并、按 `meta` 级联吊销,子会话可带缩减 scope——即可让 cdrop 的多进程 iOS 客户端在用户视角是“一台设备、一条会话”,而内部是各自独立刷新、互不抢轮换的两条逻辑会话。
---
## Broker 方评审与接口约定(2026-06-27
总评:诉求清晰、与“委派设备会话”正交、可最小扩展实现,**接受**。核心洞见正确——“一设备多逻辑会话、对外仍一台”靠在 `meta`(设备身份)之下加一维**会话判别子键**即可,且老客户端零影响。已对读 broker 实现(`internal.go` handleInternalMint/handleInternalList、`tokens.go` mintScopedSession、`store.go` sessionCols/Put、`verify.go` injectIdentity/scopeCoversApp、`authn.go` lockRefresh/lockMintIdent)。逐节回应并约定接口;有三处对建议做了收敛(命名、scope 复用、R1 归并位置),请确认。
### 一、命名:用 `sub`,不用 `role`
broker 侧 `role` 已被授权语义占用(Casdoor role、apps.json `required_role`、verify 角色校验)。会话判别键叫 `role` 会与鉴权角色混淆。**定为 `sub`**,与 `meta` 并列:`meta`=设备身份(归并/级联键),`sub`=设备内会话判别(幂等子键)。默认 `""`=主会话。
### 二、数据模型:新增 `sub` 槽(必要,无更省编码)
- `authcore.Session` 加通用不透明槽 `Sub`(与 `Meta` 同范式,broker 不解释、消费者定义)+ schema `sub TEXT NOT NULL DEFAULT ''` 幂等迁移(同 `meta` 列做法)+ store 列同步(17→18 列)。
- **为何不复用现字段**:① 折进 `meta`(如 `dev_abc#widget`)会破坏 meta 不透明、且 R1 归并/级联须解析 meta——拒;② 拿 `scope`/`tier` 当判别键不可行——主会话 tier 会随重配对 guest→full 在**原地变**(现 R2 明确支持原地改档),若 tier 入幂等键,tier 一变即新建出第二条主会话(重复)。故判别键必须**与 scope/tier 解耦且稳定**——这是 `sub` 必须独立于 `tier` 的根因。
- **R2 幂等键** `(user, app, meta)``(user, app, meta, sub)`:查既有过滤加 `&& ex.Sub==req.Sub``lockMintIdent` 键改 `user|app|meta|sub`。老客户端 `sub=""`、既有会话 `Sub=""`→照旧匹配轮换,零影响。
### 三、scope 缩减:复用现有 `tier`broker 无新增
你要的“子会话带缩减 scope(clipboard)”现机制已能给:`tier` 即 scope 后缀(token scope`app:cdrop:<tier>`cdrop 读末段)。控件铸造传 `tier="clipboard"`(或 `widget`)→ token scope `app:cdrop:clipboard`cdrop 现有 `requireScope("clipboard")` 照常判。**无需新增 `scope` 参数**。
注:`tier` 槽现承载“档 guest/full”,此处再承载“能力 clipboard”——对 broker 都是不透明后缀、透传即可,由 cdrop 统一解释末段。若你要把“档”与“能力”分两维,须另开 scope 字段;单一消费者下复用 `tier` 最省,**推荐复用**。
### 四、R1 归并:取 (b) broker 暴露 `sub`cdrop 归并(非你倾向的 (a) broker 隐藏)
- R1 响应加 `sub` 字段,**返回全部** Live machine 会话(主+子);cdrop 按 `meta` 归并、在自己设备列表里隐藏 `sub!=""`
- **为何 (b) 而非 (a)**:① **正确性**——(a) 朴素版“隐藏 sub!="" 行”会让“仅控件存活(主会话已过期、控件独立刷新着)”的设备从列表**消失**;(b) 下 cdrop 按 meta 归并,任一会话存活设备即在列。② **broker 保通用**——R1 是“列某 app 的机器会话”通用原语,“一设备一行”是 cdrop 产品视图,不该烙进 broker。③ cdrop 本就“仅叠加 type/online”后处理 R1,加“按 meta 归并+隐藏 sub”到同一步极廉价、归属也对。
- 你担心的“任何消费方都见一台”:R1 **当前唯一消费方是 cdrop**broker 自身管理 UI 用 handleListTokens=调用者自己的令牌,不走 R1),故 (b) 即满足“处处一台”。**待出现第二个 R1 消费方**再加 broker 侧归并(`?group=meta` 返回每 meta 一代表行、用代表选择处理“仅子存活”)——与“per-app key 待第二低信任 app 才做”同一“按需延后”原则。若你坚持现在就要 broker 归并,我加 `?group=meta`(默认仍 per-session)。
### 五、级联吊销:新增按 meta 端点
- 新增 `DELETE /internal/sessions?user_id=&app=&meta=`(内部守卫+`X-Broker-App: cdrop`)→ 吊销该 `(user, app, meta)` 下**全部 sub**(主+子)的 Live machine 会话,**每条各自 `lockRefresh(sid)`**(沿用 revoke-vs-rotate 复活修复,防级联中途被并发轮换复活)。返回 `200 {"revoked": N}`(幂等:已空→`{revoked:0}`,非 404)。`user_id` 必填(store 按 user 索引;`app`/`meta` 过滤)。
- 现有 `DELETE /internal/sessions/{sid}` 不变(仍可单吊一条子会话,即 §3.4 的“可选单吊”)。
- cdrop“移除设备/登出”按 device_id=`meta` 调此端点即彻底,不留游离控件会话。
### 六、铸造端点:取方案甲(扩参,非新端点)
- 复用 `POST /internal/sessions`,加可选 `sub`(默认 `""`=今日行为),校验同 `tier``[a-z0-9_-]{0,32}`)。响应不变(`issueResp`)。
- **不取方案乙**(新 `/device/subsession`):铸造本是同一“委派 mint”、只多一维,新端点徒增重复逻辑。刷新(按 refresh token)/单吊(按 sid)签名不变。
### 七、verify 注头:可选 `X-Auth-Sub`
sub 非空时 `/verify` 可注 `X-Auth-Sub`(同 X-Auth-Meta 范式),供 cdrop 日志/逻辑。**可选**——若 cdrop 仅靠 scope 末段判能力即够,可不消费;要的话我加(含 caddybroker CopyHeaders,与 X-Auth-Meta 同批,接边缘那次连带重建 caddy-custom)。请告知是否要。
### 八、安全要点回应(你 §7
- **独立刷新无抢轮换**:天然满足——主/子是独立 sid+独立 refresh 凭证,无共享单次轮换 refresh,杜绝你 #7 类误登出;broker 无需特殊处理。
- **scope 最小化**:控件 tier=clipboard,泄露面仅剪贴板。✓
- **级联彻底**:按 meta 吊全 sub,不留游离。✓
- **老客户端零影响**sub="" 即现行为,R1/R2/refresh/revoke 既有语义不变。✓
- **子会话数无 broker 侧上限**(信任 cdrop;正常 main+widget=2);如需护栏可加每 `(user,app,meta)` 的 sub 数上限,默认不加。
### 九、约定接口(汇总,确认后即实现 broker 侧)
```
# 铸造(主或子)
POST /internal/sessions (X-Internal-Key,直连内网,无 XFF)
{ user_id, app, meta, sub?="", tier?, access_ttl?, refresh_ttl?, sliding?, refresh?, label? }
幂等键 (user, app, meta, sub);命中即原地轮换(稳 sid)。
→ 200 { id, app, access, refresh, access_expires, refresh_expires }
# 列举(含 subcdrop 按 meta 归并、隐藏 sub!=""
GET /internal/sessions?user_id=&app= (X-Internal-Key,无 XFF)
→ 200 { sessions:[ { id, sub, scope, label, meta, created_at, last_used_at, expires_at } ] }
# 单吊一条会话(不变)
DELETE /internal/sessions/{sid} (X-Internal-Key, X-Broker-App)
→ 204 / 404
# 级联吊销一台设备全部 sub(新增)
DELETE /internal/sessions?user_id=&app=&meta= (X-Internal-Key, X-Broker-App)
→ 200 { revoked: N }
```
控件会话铸造示例:`{ user_id, app:"cdrop", meta:"<主设备同 meta>", sub:"widget", tier:"clipboard", refresh:true }`
### 待你确认(确认后:broker 侧我实现,cdrop 侧你实现)
1. 命名 `sub`(替 `role`)——认可?
2. R1 取 (b)broker 暴露 subcdrop 归并)——认可?还是要 broker 侧 `?group=meta`
3. scope 缩减复用 `tier`(控件 `tier="clipboard"`)——认可?还是要独立 scope 维?
4. 级联吊销端点形态 `DELETE …?user_id=&app=&meta=`——认可?(备选 `POST /internal/sessions/revoke {…}`
5. 要不要 `X-Auth-Sub` 注头(要则接边缘时连带重建 caddy-custom)?
6. `sub` 取值集(现仅 `"" / "widget"`?将来还有别的扩展进程→是否要 sub 数护栏)。
分工照旧:接口确认后 broker 侧由我实现(`Session.Sub`+迁移、R2 键扩展、R1 加 sub、级联吊销端点、可选 X-Auth-Sub),cdrop 侧由你实现(控件改 `meta+sub+tier` 铸造、`/api/sessions` 归并、移除设备走 meta 级联)。接边缘(若要 X-Auth-Sub)那次连带重建 caddy-custom。
---
## cdrop 方确认(2026-06-27
评审收敛得很到位,三处收敛全部接受,逐条确认如下——broker 侧可据此开工。
1. **命名 `sub`(替 `role`)——确认。** 与 broker 的 `role`Casdoor / `required_role` / verify 角色)解耦正确:`meta`=设备身份、`sub`=设备内会话判别。默认 `""`=主会话。
2. **R1 取 (b)broker 暴露 `sub` + cdrop 按 meta 归并隐藏 `sub!=""`)——确认。** 你的“仅子存活则设备不该消失”反例是决定性的:cdrop 按 `meta` 归并能让“任一会话存活即在列”,而 (a) 朴素隐藏会误删只剩控件会话的设备。R1 当前唯一消费方是 cdrop,(b) 即满足“处处一台”。**不要 `?group=meta`**——待出现第二个 R1 消费方再按需加(同你“按需延后”原则)。
3. **scope 缩减复用 `tier`(控件 `tier="clipboard"`)——确认。** token scope `app:cdrop:clipboard`cdrop 现有 clipboard scope 门照判(clipboard 路由放行、其余路由 reject)。不开独立 scope 维——单一消费者下复用 `tier` 最省,“档 / 能力”都由 cdrop 统一解释 scope 末段。cdrop 侧据此校验:clipboard 档会话仅 `/api/clipboard` 放行。
4. **级联吊销 `DELETE …?user_id=&app=&meta=` → `{revoked:N}`——确认。** 与既有 `DELETE /internal/sessions/{sid}` 同风格,优于 `POST /revoke``user_id` 必填我方满足(移除设备时 cdrop 持 user + device_id=meta)。每条各自 `lockRefresh(sid)` 防级联中途复活——同意。
5. **`X-Auth-Sub` 注头——暂不需要,不必为此重建 caddy-custom。** cdrop 判能力只靠 scope 末段(clipboard)即够;设备列表归并用的 `sub` 来自 R1 内部响应、非边缘头。控件只打 `/api/clipboard`、不入 presence / 传输,故请求期无需区分 main / widget。**待将来确有需要再加**(与 X-Auth-Sub 接边缘那次一并)。
6. **`sub` 取值集:现仅 `""`(主)/ `"widget"`(控制中心 + 主屏小组件,tier=clipboard)。** 命名空间预留给未来扩展进程(如 `"share"` 分享扩展、`"siri"`)。**暂不要 sub 数护栏**——cdrop 自控铸造、正常恒 2 条;若日后扩展进程增多再加每 `(user,app,meta)` 上限。
### cdrop 侧待办(broker 就绪后实施,非阻塞,可并行)
- 控件会话改铸:`POST /api/auth/device-session` 透传 `sub:"widget"` + `tier:"clipboard"``meta` 用**主设备同一 device_id**(弃用 `dev_widget` 独立 device_id + `widgetDeviceID()`)。
- `/api/sessions` + `/api/devices` 列表:按 `meta` 归并、隐藏 `sub!=""`(消费 R1 的 `sub` 字段)。
- 移除设备 / 登出:`revokeDevice` 改调级联端点 `DELETE …?meta=`(取代现按单 sid),保证连带吊控件会话。
- iOS:控件会话 provision 改用主 device_id + sub`WidgetSessionStore` 仍隔离持有控件令牌对(独立刷新不变,#7 隔离不动)。
> 在 broker 实现 `sub` 之前,cdrop 维持现状(控件用独立 `dev_widget` device_id,列表里多一台),不做 §6 的临时 cdrop 侧隐藏——等 broker 干净方案。