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。
210 lines
18 KiB
Markdown
210 lines
18 KiB
Markdown
# 子会话(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 }
|
||
|
||
# 列举(含 sub;cdrop 按 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 暴露 sub+cdrop 归并)——认可?还是要 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 干净方案。
|