开启 Cross-Origin Isolation 后,我的网站社会性死亡了

为了用 SharedArrayBuffer 提升那该死的性能,我听从了 MDN 的建议开启 COOP/COEP。然后,整个网站社会性死亡了。

0. 动机

我在做 AudioWorklet + SharedArrayBuffer 的无锁通信。SAB 是唯一能让主线程和音频线程共享内存的原生方案——没有它,每帧都要 postMessage 序列化,延迟直接翻倍。

但 SAB 有个前提:浏览器要求页面必须开启 Cross-Origin Isolation。也就是在响应头里加上:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

MDN 说这是"最佳实践"。Chrome 的控制台也在疯狂警告你不开就用不了 SAB。于是我就开了。

然后网站炸了。

1. 社会性死亡现场

1.1 OAuth 登录阵亡

GitHub OAuth 弹窗登录,点完授权,回调页面白屏。window.openernull

原因很简单:COOP: same-origin 会切断跨域窗口之间的引用。你的 OAuth 弹窗和主页面不同源,window.opener 直接被浏览器置空。授权码拿不回来,登录流程断裂。

这不是 Bug,是隔离的物理代价。

1.2 第三方 SDK 变僵尸

Google Analytics 不报数据了。Sentry 不捕获错误了。不是它们挂了,是 COEP: require-corp 把所有不带 Cross-Origin-Resource-Policy 响应头的跨域资源全部拦截了。

你的页面加载了 analytics.google.com/ga.js,这个脚本没有 CORP 头,浏览器直接拒绝执行。GA 就这样无声无息地死了——没有错误,没有降级,就是静默失败。

1.3 媒体黑屏

CDN 上的图片全变黑块。<img src="https://cdn.example.com/photo.jpg"> 加载不出来。原因同上:CDN 的图片响应没有 Cross-Origin-Resource-Policy 头,被 COEP 一刀切了。

你能控制自己的 Nginx,但你控制不了别人的 CDN。这就是隔离最毒的地方:它的限制是全局的,不区分"你的资源"和"你引用的资源"。

2. 为什么会这样

这一切的根源是 Spectre

2018 年的 Spectre 漏洞证明了:恶意 JavaScript 可以通过侧信道攻击读取同一进程内其他域名的内存。为了防御,Chrome 实施了 Cross-Origin Isolation——用进程级隔离确保不同源的资源不会出现在同一渲染进程里。

代价是:所有跨域资源都必须显式声明"我允许被嵌入"。不声明的,一律拦截。这就是 COEP 的逻辑。

而 COOP 切断 window.opener,是为了防止跨域窗口通过 window.opener 访问原始页面的 DOM。这是同源策略在隔离模式下的强化版。

3. 基础修复

3.1 自己的资源:Nginx 配置

对于你能控制的资源,在 Nginx 里加上 CORP 头:

# Nginx
add_header Cross-Origin-Resource-Policy "cross-origin" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;

这样你自己的图片、脚本、样式就不会被 COEP 拦截。

3.2 OAuth 回调:Credentialless 模式

Chrome 96+ 支持 Cross-Origin-Embedder-Policy: credentialless。这个模式允许不带凭证加载跨域资源,同时保留 COEP 的隔离语义。OAuth 弹窗在这个模式下可以正常回调。

# 替换 require-corp 为 credentialless
add_header Cross-Origin-Embedder-Policy "credentialless" always;

3.3 第三方 SDK:CSP 白名单

对于 GA、Sentry 这类必须执行的跨域脚本,可以用 crossorigin 属性显式声明:

<script src="https://analytics.google.com/ga.js" crossorigin></script>

但这只是声明意图,最终能不能加载还是取决于对方服务器的 CORS 配置。如果对方不支持 CORS,你只能走 Service Worker。

4. Service Worker:给第三方资源"办签证"

这是我找到的最可靠的方案。

原理:Service Worker 可以拦截页面发出的所有请求,包括跨域的。在 SW 里,你可以给任何响应补上缺失的 COEP/CORP 头——相当于在客户端侧给第三方资源"补办签证"。

// service-worker.js
self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request).then((response) => {
      // 只给缺少 CORP 头的跨域响应补头
      if (!response.headers.has('Cross-Origin-Resource-Policy')) {
        const newHeaders = new Headers(response.headers)
        newHeaders.set('Cross-Origin-Resource-Policy', 'cross-origin')
        return new Response(response.body, {
          status: response.status,
          statusText: response.statusText,
          headers: newHeaders,
        })
      }
      return response
    })
  )
})

这样,即使第三方 CDN 不支持 CORP,你的 Service Worker 也能在客户端侧把缺失的头补上。页面正常加载,SAB 正常工作,隔离也保持完整。

注意:这个方案只适用于公开资源(图片、公开 JS)。涉及凭证的 OAuth 流程,还是得走 Credentialless 模式。

5. 交互式沙盒

cross-origin-isolation-sandboxNO ISOLATION
Cross-Origin-Opener-Policy
Cross-Origin-Embedder-Policy
Service Worker 签证代理
🧠 SharedArrayBuffer需要开启严格隔离
🏠 同源自身资源同源资源不受限制
🔌 第三方无 CORP 资源 (CDN/SDK)放行
🔑 OAuth 弹窗 (window.opener)opener 可用
🖼️ 跨域图片/媒体放行

🔴 隔离未开启 — SharedArrayBuffer 不可用

6. 完整的隔离策略

把以上方案组合起来,一份生产级配置:

# Nginx:开启隔离
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "credentialless" always;

// Service Worker:给公开资源补 CORP 头
// (见上方代码)

// 页面中:显式声明 crossorigin
<script src="https://cdn.example.com/lib.js" crossorigin></script>
<img src="https://cdn.example.com/photo.jpg" crossorigin />

COOP: same-origin 隔离窗口引用。COEP: credentialless 允许 OAuth 回调。Service Worker 补齐第三方资源的 CORP 头。三层配合,隔离生效,功能不残。

7. 底线

Cross-Origin Isolation 不是可选项——如果你要用 SharedArrayBuffer,它就是强制的。但隔离的代价是真实的:OAuth 会断、SDK 会死、图片会黑屏。

这些不是 Bug,是浏览器在 Spectre 时代筑起的柏林墙。你推不倒它,但你可以学会在墙这边过日子。

Service Worker 办签证、Credentialless 留后路、Nginx 配自己的地盘。三条路走通,隔离世界就能活。


在线实验:STW Sentinel Lab
NPM:npm i stw-sentinel
GitHub:hlng2002/stw-sentinel