一个 Chrome 窗口跑多个隔离工作台,技术上是怎么做到的

单窗口隔离靠三层叠加:会话与存储层、网络路由层、生命周期管理层,而不是内核级虚拟化。它不改内核指纹。

为什么“一个浏览器开多个窗口”不等于多个账号同时登录?

先拆掉一个直觉:窗口不是隔离单位。同一个 Profile 里开十个窗口,它们共用同一份 Cookie 库、同一份 localStorage 和同一个 HTTP 缓存,站点看到的仍是同一个会话;隐身窗口确实有独立存储,但只有一个,关窗即弃,没法给十个账号各配一个。真正的隔离单位是“会话容器”:一份独立的 Cookie 集合与存储命名空间,加一条稳定的归属映射。Chrome 没有现成的容器 API(Firefox 的 contextualIdentities 才有),所以扩展只能自己在会话层造一个——先记住“哪个标签页属于哪个容器”,再在请求与页面两个面上按这张映射分流。CrispLogin 这类扩展就是按这个思路做的:容器是扩展自己维护的归属,不是 Chrome 的原生概念。所以隔离的粒度是“会话”,不是“进程”。

第一层:Cookie 为什么不串?

不串号靠的是“虚拟 Cookie 罐 + 按标签页改写请求头”。每个容器在扩展里有一份自己的 Cookie 记录;请求发出前,declarativeNetRequest 的 session rule 用 condition.tabIds 匹配标签页,把 cookie 请求头整体 set 成这个容器的值;响应回来时,再用 webRequest 观察 Set-Cookie 写回对应的罐。罐里的 Cookie 得按 path 长度与创建时间自行排序序列化,拼出来的头才和浏览器原生的一致。

容易漏的是生命周期:tabIds 只对 session rule 有效,而 session rule 在浏览器退出、扩展更新时会被清空,所以每次启动都要重装规则,漏了就表现为“重启后所有工作台变成一个号”。页面存储是另一个面——document.cookie 读的是真实 Cookie 库,localStorage 也只有一个命名空间,得靠 MAIN world 脚本按容器代理读写。这层是应用层隔离:分得开同站点的两个账号,但不改内核指纹。

第二层:代理怎么跟着工作台走?

Chrome 没有“按标签页设代理”的 API。chrome.proxy.settings 的作用域是整个 Profile,PAC 的 FindProxyForURL(url, host) 拿不到 tabId;declarativeNetRequest 能改请求头、能重定向,却没有“选上游代理”这个动作。所以扩展能做的是维护一张“工作台 → 出口”的映射,并在前台标签页变化时改写这份全局配置。

PAC 可以按域名分流,但依据是 URL 不是标签页——同一个站点在两个工作台里,PAC 给不出两个答案。因此“十个工作台各跑一条隧道”在扩展里不成立,能保证的是“此刻前台是谁,出口就是谁的”。这也是切换工作台时会看到一次重连的原因:代理配置一改,已建立的连接要重建。边界:这层换的是出口 IP 与请求路径,TCP/TLS 指纹(JA3/JA4)不变;WebRTC 可以用 chrome.privacy.network.webRTCIPHandlingPolicy 关掉非代理 UDP,但那是浏览器级策略,不按工作台生效。

第三层:关掉标签页、重启浏览器,绑定还在吗?

MV3 的后台是 Service Worker:30 秒没有事件就被终止,模块级变量和闭包里的 Map 一起消失。所以“哪个标签页属于哪个工作台”不能存内存,必须落到 chrome.storage.session——它在扩展加载期间驻留内存,SW 被回收再唤醒后仍在,浏览器重启即清空。注意它默认不对内容脚本开放,要让页面侧读到归属,得显式调一次 setAccessLevel。

这就分出两种数据:运行时绑定放 session,工作台列表、代理配置、快照索引这些跨重启要用的放 storage.local。因果链很直接:因为绑定落在 session 而不是变量里,唤醒后扩展仍知道每个标签页的归属,不会把两个工作台的 Cookie 灌到同一页面;只存内存的话,SW 一睡,下一次请求就没有归属可查,表现是分组重复、快照灌错容器。

第四层:站点快照存的是什么,恢复的又是什么?

快照解决的是另一个问题:登录态的可搬运,不是运行时的隔离。它排在第四层,但不属于隔离本身。它把某个站点在一个容器里的 Cookie 与该站点的存储序列化存下来,恢复时灌回这个容器的命名空间。两者正交:隔离保证两个工作台同时存在时互不干扰,快照保证不用重新登录就能回到某个状态。快照的粒度是站点而不是整个浏览器:同一容器里两个站点,是两条互不相干的记录。

所以快照必须从属于容器——把 A 的登录态灌进 B,等于把两个号混成一个。也要写明它不做什么:恢复的是应用层会话状态,不恢复内核级指纹,也不承诺绕过平台的风控与设备校验。用法见【内链:站点快照的实际用法】。

和“开多个 Profile”比,单窗口多工作台差在哪?

Chrome 的多 Profile 是存储根目录级的物理切割:每个 Profile 一套独立的 Cookie 库、存储、扩展与缓存,隔离彻底。代价落在四个维度上——内存,每个 Profile 有自己的网络上下文与渲染进程,开三个就近似三份常驻开销;进程,进程树各自独立;窗口,一个 Profile 一个窗口,切换等于换窗口;指纹,Profile 之间共享同一份内核与硬件,UA、Canvas、字体默认完全一致。另一笔隐性成本是数据分裂:书签、扩展、历史默认不共享,三个以上就开始各自为政。

扩展方案反过来:同一个 Profile、同一个窗口,共享进程与内核,换来单窗口并行和近乎为零的增量开销,代价是碰不到内核指纹层。所以两者不是同一件事的两个档位,而是隔离粒度的两种取法。展开见【内链:Chrome 多 Profile 与扩展隔离的对比】。

能力边界:哪些层扩展能覆盖,哪些覆盖不到?

把上面的边界收成一张表。读法是“看平台在查什么”:查会话与出口,看中间那一列;查指纹一致性,看右边那一列。需要强调:扩展做的是会话层隔离,和指纹浏览器不是同等级别的隔离,把它当成“更轻的指纹浏览器”会误判风险。

扩展级会话隔离的能力边界(2026-11 版本)
层扩展能覆盖覆盖不到,或代价在哪
Cookie 与登录态按容器分开,可序列化成快照内核不认容器,靠请求头改写;规则丢了就串号
页面存储localStorage、IndexedDB 可按容器加前缀或代理Service Worker、SharedWorker 等独立上下文要单独覆盖
出口 IP按工作台绑定,切前台即切出口同一时刻只有一个前台出口,不能多隧道并发
WebRTC可关闭非代理 UDP(浏览器级策略)不按工作台生效,媒体路径仍可能绕开代理
TLS 指纹无JA3/JA4 由内核与网络栈决定
Canvas / WebGL / Audio无渲染发生在内核,扩展没有改写接口
字体枚举无只能在页面 JS 层打补丁,可被检测
HTTP 缓存无缓存是 Profile 级,容器之间共享

这张表也说明了能力上限在哪:扩展能替代的是“为了分开登录而买客户端”的动机,不是客户端本身。两条路线怎么选,是另一个问题,逐项对照见【内链:扩展与指纹浏览器的路线对比】。

常见问题

扩展做的 Cookie 隔离和 Chrome 原生的 Partitioned Cookie 是一回事吗?

不是。Partitioned Cookie 按顶层站点分键,同一站点在不同顶层页面下各有一份;cookies API 里的 partitionKey 只是读写这类分区 Cookie 用的键,键只有 topLevelSite,不能拿它另开分区。工作台的罐按容器分,同一站点可以有任意多份,且由扩展自己记账。前者是浏览器的隐私分区,后者是扩展的会话抽象。

关掉标签页后工作台状态还在吗?

分三种。标签页本身没了,绑定关系随之失效;工作台的定义(名称、代理、快照索引)这类跨重启要用的数据一般落在 storage.local;登录态只有在存过快照时才可恢复,否则要重新登录。storage.session 里的东西浏览器一关就清空,别把它当持久层。

单窗口方案会影响浏览器性能吗?

增量主要来自标签页本身,而不是隔离机制。扩展不启第二个浏览器、不复制内核,开销是规则匹配与请求头改写,属于每请求一次的常数级工作。真正的成本在标签页数量与页面本身:开二十个工作台,卡的是渲染,不是隔离层。

两个工作台能同时走不同的出口 IP 吗?

不能。代理配置是 Profile 级的,扩展只能在标签页切换时改写它,所以同一时刻只有前台工作台走自己的出口;后台工作台要么被规则挡住,要么直连。要真正的多隧道并发,得用带独立网络栈的客户端方案。

资料来源与最后更新

本文涉及 API 行为的部分以官方文档为准;扩展能力边界随 Chrome 版本变化,落地前请按当期文档复核。

最后更新:2026 年 11 月 11 日。文中写的是这类扩展的通用实现路径,不同产品选用的 API 与数据落点可能不同;涉及具体产品的内部架构,以该产品文档为准。