WEBKT

纯静态托管的救星:用 Service Worker 轻松搞定跨源隔离与静态资源跨域拦截

7 0 0 0

在现代 Web 开发中,尤其是涉及 WebAssembly、SharedArrayBuffer 多线程操作或高性能定时器(如 performance.now() 精确度要求)的场景下,浏览器要求页面必须处于**跨源隔离(Cross-Origin Isolated)**状态。

要开启跨源隔离,传统做法是在服务器端配置以下响应头:

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

然而,当我们使用 GitHub Pages、Vercel、Netlify 等纯静态托管平台,或者在无法修改后端路由响应头的内网/第三方环境中,配置这些 Header 变得异常困难。更糟糕的是,一旦强行开启了 COEP(require-corp),页面中引入的第三方 CDN 静态资源(如图片、JS、字体)如果未配置 Cross-Origin-Resource-Policy 响应头,就会直接被浏览器强行拦截,导致页面崩溃。

本文将分享一种无需依赖任何后端服务器配置、纯前端的黑魔法:利用 Service Worker(SW)在客户端动态拦截并注入响应头,完美解决跨源隔离与第三方资源拦截问题。


核心原理:把 Service Worker 当作“本地代理服务器”

Service Worker 拥有拦截当前 Scope 下所有网络请求(fetch 事件)的能力。通过编写特定的 SW 脚本,我们可以在浏览器真正解析响应之前,劫持并修改响应头:

  1. 欺骗浏览器开启跨源隔离:当浏览器请求主 HTML 文档时,SW 拦截该请求并强行加上 COOPCOEP 响应头。浏览器收到后,会认为服务器支持该标准,从而允许该页面上下文进入 crossOriginIsolated 状态(解锁 SharedArrayBuffer)。
  2. 拯救被拦截的第三方资源:当页面加载第三方 CDN 资源时,SW 拦截这些静态资源的响应,并动态为其注入 Cross-Origin-Resource-Policy: cross-origin 响应头,阻止浏览器因 COEP 规则将其拦截。

极简落地实现方案

我们需要两个文件:一个嵌入在 index.html 中的引导注册脚本,以及一个独立的 coi-service-worker.js 脚本。

第一步:编写 Service Worker 核心拦截器 (coi-service-worker.js)

将以下代码保存为 coi-service-worker.js,并放置在项目根目录下(确保其 Scope 覆盖整个站点):

// coi-service-worker.js
self.addEventListener("install", () => {
  // 强制跳过等待,激活后立即接管页面
  self.skipWaiting();
});

self.addEventListener("activate", (event) => {
  // 确保 Service Worker 激活后立即控制所有客户端
  event.waitUntil(self.clients.claim());
});

self.addEventListener("fetch", (event) => {
  const { request } = event;

  // 忽略非同源的某些特定请求(如 chrome-extension 等)
  if (request.url.startsWith("chrome-extension://") || request.url.includes("extension")) {
    return;
  }

  event.respondWith(
    fetch(request)
      .then((response) => {
        // 如果响应是 opaque(无 CORS 授权的跨域资源),无法直接读取/修改其 header
        // 这里必须判断 status 是否为 0。如果是 0 且资源因 COEP 被拦截,需要确保该资源支持 CORS
        if (response.status === 0) {
          return response;
        }

        // 复制一份响应,因为原始响应的 Headers 是只读的
        const newHeaders = new Headers(response.headers);

        // 1. 为主文档和同源请求注入跨源隔离头
        newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
        newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
        
        // 2. 为静态资源(JS, CSS, 图片等)注入 CORP 头,防止被 COEP 规则拦截
        newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");

        return new Response(response.body, {
          status: response.status,
          statusText: response.statusText,
          headers: newHeaders,
        });
      })
      .catch((err) => {
        // 降级处理:若请求失败,尝试原样返回
        return fetch(request);
      })
  );
});

第二步:在页面中嵌入引导与自动重载逻辑

由于 Service Worker 首次注册时页面已经加载完毕(此时页面并未处于隔离状态),我们需要在注册成功后自动刷新一次页面,让激活后的 SW 接管主文档的加载。

在你的入口 index.html<head> 标签最顶部,插入以下脚本:

<script>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('./coi-service-worker.js')
        .then((registration) => {
          console.log('COI Service Worker 注册成功:', registration.scope);

          // 核心逻辑:如果当前没有处于跨源隔离状态,且 SW 已经激活完毕,则刷新页面
          if (!window.crossOriginIsolated) {
            // 监听 SW 状态变化,确保在 active 状态下执行刷新
            if (registration.active) {
              window.location.reload();
            } else {
              registration.addEventListener('updatefound', () => {
                const installingWorker = registration.installing;
                installingWorker.addEventListener('statechange', () => {
                  if (installingWorker.state === 'activated' && !window.crossOriginIsolated) {
                    window.location.reload();
                  }
                });
              });
            }
          }
        })
        .catch((err) => {
          console.error('COI Service Worker 注册失败:', err);
        });
    });
  }
</script>

关键技术细节与避坑指南

1. 为什么我的第三方图片/JS 依然报错 net::ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultCoep

当开启了 require-corp 之后,加载任何第三方资源必须满足以下两个条件之一:

  • 条件 A:该资源响应头包含 Cross-Origin-Resource-Policy: cross-origin(由我们的 SW 劫持并注入)。
  • 条件 B:该资源通过 CORS 模式加载(即 <script src="..." crossorigin>),且服务器返回了 Access-Control-Allow-Origin

避坑点:如果第三方服务器明确拒绝 CORS(即没有返回 Access-Control-Allow-Origin),并且你的请求使用了 crossorigin 属性,请求本身会报 CORS 错误。如果你的请求没有crossorigin(属于不透明请求 / opaque request),SW 拿到的 response.status 将会是 0
对于 status === 0 的响应,JS 无法读取也无法修改其 Headers。因此,对于非 CORS 的第三方绝对路径资源,SW 无法为其注入 CORP 头。

解决方案

  • 尽量将静态资源本地化(放到同源服务器/打包目录下)。
  • 对于必须使用的 CDN 资源(如 Google Fonts, 百度统计等),确保引入时加上 crossorigin="anonymous" 属性,并保证 CDN 服务商本身支持 CORS。

2. 作用域(Scope)限制

Service Worker 的默认最大作用域是由其脚本所在的目录决定的。如果你把 coi-service-worker.js 放到了 /assets/js/ 下,它将无法拦截根目录 /index.html 的请求,从而导致主文档无法被注入 COOP/COEP 头。

  • 铁律:务必将 SW 脚本放在根目录下。

3. 本地开发环境(Dev Server)下的处理

大多数现代前端脚手架(如 Vite、Webpack Dev Server)默认支持通过配置直接开启 COOP/COEP。在本地开发时,建议直接通过脚手架配置,仅在打包后的静态生产环境(如 GitHub Pages)中启用此 Service Worker 方案。

Vite 为例,本地配置非常简单:

// vite.config.js
export default {
  server: {
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
};

总结

通过本方案,我们成功在不拥有服务器控制权的前提下:

  1. 欺骗浏览器激活了 window.crossOriginIsolated,让诸如 WebAssembly 线程、高性能计时器得以正常工作。
  2. 动态改写了静态资源的响应头,解决了静态托管平台部署 Godot/Unity 网页版、FFmpeg.wasm 时的跨域阻碍。
  3. 保证了在刷新后,用户的二次访问能无缝、无感知地进入隔离沙盒环境。
极客飞弹 跨源隔离跨域拦截

评论点评