Files
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

18 KiB
Raw Permalink Blame History

子会话(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 铸造端点加两个可选参数 rolescope。缺省 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 侧过滤(不改 Brokercdrop 在自己的 /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.SublockMintIdent 键改 user|app|meta|sub。老客户端 sub=""、既有会话 Sub=""→照旧匹配轮换,零影响。

三、scope 缩减:复用现有 tierbroker 无新增

你要的“子会话带缩减 scope(clipboard)”现机制已能给:tier 即 scope 后缀(token scopeapp:cdrop:<tier>cdrop 读末段)。控件铸造传 tier="clipboard"(或 widget)→ token scope app:cdrop:clipboardcdrop 现有 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 当前唯一消费方是 cdropbroker 自身管理 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 的 roleCasdoor / 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:clipboardcdrop 现有 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 /revokeuser_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 + subWidgetSessionStore 仍隔离持有控件令牌对(独立刷新不变,#7 隔离不动)。

在 broker 实现 sub 之前,cdrop 维持现状(控件用独立 dev_widget device_id,列表里多一台),不做 §6 的临时 cdrop 侧隐藏——等 broker 干净方案。