- 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。
18 KiB
子会话(subordinate session)— 提交 Auth Broker 评审
在现有“委派设备会话”(R1 列举 + R2 幂等铸造,
meta为稳定设备身份)之上,引入“子会话”: 一台设备下可有多条独立令牌的逻辑会话,但对外仍呈现为一台设备、一条会话。 状态:待 Broker 评审。关联:委派设备会话模型、cdropinternal/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)(权威端归并最干净,任何消费方都见一台)。
- (a) R1 直接隐藏
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_widgetdevice_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 不解释、消费者定义)+ schemasub 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 侧你实现)
- 命名
sub(替role)——认可? - R1 取 (b)(broker 暴露 sub+cdrop 归并)——认可?还是要 broker 侧
?group=meta? - scope 缩减复用
tier(控件tier="clipboard")——认可?还是要独立 scope 维? - 级联吊销端点形态
DELETE …?user_id=&app=&meta=——认可?(备选POST /internal/sessions/revoke {…}) - 要不要
X-Auth-Sub注头(要则接边缘时连带重建 caddy-custom)? 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 侧可据此开工。
- 命名
sub(替role)——确认。 与 broker 的role(Casdoor /required_role/ verify 角色)解耦正确:meta=设备身份、sub=设备内会话判别。默认""=主会话。 - R1 取 (b)(broker 暴露
sub+ cdrop 按 meta 归并隐藏sub!="")——确认。 你的“仅子存活则设备不该消失”反例是决定性的:cdrop 按meta归并能让“任一会话存活即在列”,而 (a) 朴素隐藏会误删只剩控件会话的设备。R1 当前唯一消费方是 cdrop,(b) 即满足“处处一台”。不要?group=meta——待出现第二个 R1 消费方再按需加(同你“按需延后”原则)。 - scope 缩减复用
tier(控件tier="clipboard")——确认。 token scopeapp:cdrop:clipboard,cdrop 现有 clipboard scope 门照判(clipboard 路由放行、其余路由 reject)。不开独立 scope 维——单一消费者下复用tier最省,“档 / 能力”都由 cdrop 统一解释 scope 末段。cdrop 侧据此校验:clipboard 档会话仅/api/clipboard放行。 - 级联吊销
DELETE …?user_id=&app=&meta=→{revoked:N}——确认。 与既有DELETE /internal/sessions/{sid}同风格,优于POST /revoke。user_id必填我方满足(移除设备时 cdrop 持 user + device_id=meta)。每条各自lockRefresh(sid)防级联中途复活——同意。 X-Auth-Sub注头——暂不需要,不必为此重建 caddy-custom。 cdrop 判能力只靠 scope 末段(clipboard)即够;设备列表归并用的sub来自 R1 内部响应、非边缘头。控件只打/api/clipboard、不入 presence / 传输,故请求期无需区分 main / widget。待将来确有需要再加(与 X-Auth-Sub 接边缘那次一并)。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_widgetdevice_id,列表里多一台),不做 §6 的临时 cdrop 侧隐藏——等 broker 干净方案。