
如果你在项目里遇到过SharedArrayBuffer is not defined这类报错那你应该已经搜索过不少资料答案大概率指向同一个词跨域隔离cross-origin isolation。我去年帮团队优化一个浏览器端图片批量处理工具时就因为这个限制卡了两天把 COOP/COEP 的组合逻辑彻底弄明白之后才真正打通。今天这篇文章我不念文档只讲实操——它是什么、为什么这么设计、在 Nginx/Express/Vite 里怎么加、以及加上之后哪些坑等着你。1. SharedArrayBuffer 被“上锁”的前因后果1.1 SharedArrayBuffer 到底能干什么SharedArrayBuffer 是 ES2017 标准里引入的共享内存对象可以简单理解成一块“主线程和 Worker 线程都能直接读写”的内存区域。普通数据在 postMessage 的时候要经历结构化克隆数据量一大就有明显开销而 SharedArrayBuffer 不用复制两份线程拿着同一块内存地址读写都是同一份数据。它最常出现在这几类场景里图像处理主线程拿到图片像素把像素数组塞进 SharedArrayBufferWorker 直接做滤镜、缩放、人脸检测处理完主线程立刻能看到结果。音视频解码WebCodecs、Web Audio 这类 API 处理连续帧数据时用共享内存可以省掉大量帧拷贝。WebAssembly 高性能计算WASM 实例的内存本身就是 ArrayBuffer可以和 SharedArrayBuffer 打通让多个线程直接访问。游戏、云渲染、协同编辑这类低频高流量场景。如果你不需要多线程共享数据只是想在 Worker 里跑任务那普通 postMessage 也够用。SharedArrayBuffer 是给“性能敏感”的场景准备的这也是它后来被浏览器严格管控的原因。1.2 一个处理器漏洞引发的连锁反应2018 年开年安全界投下两颗深水炸弹Spectre幽灵和 Meltdown熔断。这类处理器漏洞利用分支预测和缓存命中的时间差异可以跨进程读取本不该访问的内存内容。问题在于浏览器里恰好有几种能力可以被组合成一条利用链SharedArrayBuffer 提供共享内存Atomics 提供原子操作performance.now()提供高精度计时器。攻击者可以把目标数据读入共享内存用计时器测量缓存访问时间从而一点点还原出敏感信息。Chrome 68 开始默认禁用了 SharedArrayBufferFirefox、Safari 也陆续跟随导致一大批依赖共享内存的 Web 应用被砍掉功能。浏览器厂商的解决思路是“用安全换性能”只要页面处于一个可以信任的隔离环境就重新开放 SharedArrayBuffer。这个“可以信任的隔离环境”就是跨域隔离。它要求页面同时声明两个 HTTP 响应头COOP 和 COEP。只要这两个头设置正确window.crossOriginIsolated就会变成 trueSharedArrayBuffer 重新可用performance.now()的分辨率也会回到微秒级。注意这里的关键是“同时”。单独设置 COOP 或者单独设置 COEP 都不能让 crossOriginIsolated 生效必须两个头都符合条件。2. 跨域隔离的核心COOP 与 COEP 各管一块2.1 COOP 管的是窗口关系COOP 全称 Cross-Origin-Opener-Policy它管的是“当前窗口和其他窗口之间的引用关系”。默认情况下一个页面用window.open打开另一个跨源页面返回的引用会连到那个窗口的 DOM这就是window.opener机制。恶意站点可以把你引导进一个隐藏页面再通过 opener 关系操控你的窗口或者反过来读取你的敏感信息。COOP 能切断这层关系Cross-Origin-Opener-Policy: same-origin当响应头设置为same-origin时当前页面会被放进一个隔离的“浏览上下文组”里和其他非同源窗口不再共享 opener跨源 window.open 返回的引用基本归 null。想让 SharedArrayBuffer 恢复可用COOP 必须取same-origin不能只设same-origin-allow-popups也不能用默认的unsafe-none。这里有个实践中容易困惑的点设置了 COOP 之后你项目里那些“弹窗登录后父页面要刷新一下”的代码可能就失效了。因为弹窗和父页面之间的 opener 关系被切断父页面拿不到弹窗里传回来的信号。解决办法也很统一改用 postMessage 或者在弹窗里直接通知服务器父页面通过接口轮询状态。2.2 COEP 管的是子资源加载COEP 全称 Cross-Origin-Embedder-Policy它管的是“当前页面里所有嵌入子资源是否被明确许可”。这里说的子资源包括图片、脚本、样式表、字体、iframe、Worker 脚本等。跨域隔离环境要求页面里不能出现任何“来源不明的跨源资源”否则攻击者可能趁乱加载恶意数据参与侧信道攻击。标准写法Cross-Origin-Embedder-Policy: require-corprequire-corp意味着页面加载的所有跨源资源都必须通过以下两种方式之一声明“我愿意被跨源加载”响应自带Cross-Origin-Resource-Policy简称 CORP头值可以是same-origin、same-site或cross-origin。资源支持 CORS响应里带着Access-Control-Allow-Origin并且资源标签上加了crossorigin属性。如果你在 devtools 里看到某个图片被拦截大概率就是这些条件没满足。同源资源不受影响本地开发时正常加载的本地资源一般不会踩坑。2.3 COEP 的另一个取值credentialless很多实际项目根本没有办法控制所有第三方资源的响应头。CDN 上的老脚本、广告平台的上报图片、某地图服务的 aysnc 加载都不可能给你加 CORP。如果硬上require-corp页面里会出现大量资源被拦截的情况。COEP 提供了第二种模式Cross-Origin-Embedder-Policy: credentiallesscredentialless 的意思是跨源子资源仍然可以加载但所有跨源请求都不携带凭据比如 Cookie相当于自动把所有跨源请求降级为“匿名跨源”。这样既缓解了携带凭据时的敏感信息泄露风险也免去了逐个给第三方资源加头的烦恼。我自己的建议是如果项目里第三方资源不多优先用require-corp安全性更高如果接入了一堆你控制不了的外部脚本、iframe、图片先上credentialless把功能跑通再逐步把关键资源改成require-corp。现在 Chrome、Safari 对 credentialless 支持已经比较完整Firfox 主流版本也没有障碍。3. 实操从零配置跨域隔离3.1 在 Nginx 里设置响应头如果你是常规 Nginx 部署配置相当直接在要启用跨域隔离的 server 块里加上两个 add_header 就可以server { listen 443 ssl; server_name your-domain.com; add_header Cross-Origin-Opener-Policy same-origin always; add_header Cross-Origin-Embedder-Policy require-corp always; root /var/www/html; index index.html; }always参数的作用是保证即使响应状态码是 404、302 等非 200 情况响应头也会带上。不加 always 的情况下Nginx 只会在 200、201、204、206、301、302、303、304、307、308 这几种状态里附加 add_header错误页和部分重定向就不会带可能导致排查半天找不出原因。如果你在某个 location 里单独配置了 add_header一定要小心 Nginx 的继承覆盖问题。Nginx 规定如果一个 location 中出现任何 add_header 指令更外层所有的 add_header 都会被忽略。我见过不少踩坑案例server 级别设置了 COEPlocation 里为了给某个接口加 Cache-Control 又写了一个 add_header结果 COEP 头全丢了。解决办法是外层保持统一location 里需要加头时把 COOP/COEP 也一并写进去或者把公共头提取到单独配置文件里 include。3.2 在 Express / Node.js 里设置响应头前后端同域部署的 Node 服务用中间件统一加const express require(express); const app express(); app.use((req, res, next) { res.setHeader(Cross-Origin-Opener-Policy, same-origin); res.setHeader(Cross-Origin-Embedder-Policy, require-corp); next(); }); app.use(express.static(public)); app.listen(3000, () { console.log(server running at http://localhost:3000); });这里需要注意静态资源中间件要放在设置头的中间件之后否则某些静态文件可能已经返回了才拿到头。实际上只要顺序正确所有响应都会带上这两个头。如果你想对 API 请求和静态页面区别对待可以在中间件里按路径判断但通常没必要。3.3 在 Vite / Webpack 开发服务器里设置本地开发时最常见的痛点是“生产环境配好了开发环境没有”导致 SharedArrayBuffer 跑不起来。Vite dev server 可以这样配// vite.config.js export default { server: { headers: { Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: require-corp, }, }, };Webpack devServer 的写法module.exports { devServer: { headers: { Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: require-corp, }, }, };开发环境下如果你用了跨域的前端代理 /apiCOEP 不会拦代理接口因为 fetch 请求天然走 CORS 逻辑和嵌入资源不是一回事。真正需要关注的是图片、Worker、iframe 这类嵌入资源本地开发如果碰到拦截先看 devtools 里的具体报错再决定是加 CORP 头还是换 credentialless。3.4 静态资源如何配合 CORP如果你用的 COEP 是require-corp需要给可能被跨域加载的静态资源加上 CORP 响应头location /assets/ { add_header Cross-Origin-Resource-Policy cross-origin always; }这个头同样可以在服务器代码里统一加app.use(/assets, (req, res, next) { res.setHeader(Cross-Origin-Resource-Policy, cross-origin); next(); });CORP 的取值有三个档位same-origin只有同源资源可以加载。same-site同站点可以跨子域可以加载。cross-origin所有跨源都允许。如果你不确定资源的来源最宽松的cross-origin能避免大部分资源被拦截但安全性打折扣。更好的做法是区分场景字体、CDN 静态文件设成cross-origin内部接口资源保持默认。注意CORP 是响应头它不会因为你页面里加了 crossorigin 属性就自动改变。CORP 和 CORS 是两套独立机制CORP 是资源“单方面声明允许跨源”CORS 是请求方发起的“询问式许可”。COEP 只要满足其中任何一套资源就不会被拦。3.5 还有一条路Service Worker 代理如果项目里第三方资源实在多到改不动又不能用 credentialless还有一个压箱底方案用 Service Worker 拦截请求在响应里补加 CORP 头。常见思路是给所有跨源响应包一层new Response然后手动加上Cross-Origin-Resource-Policy: cross-origin。但这要求你对流量做仔细过滤不要把所有响应都无脑加头否则隔离效果等于没有。Service Worker 方案适合有专门前端基建团队的场景小项目不太建议复杂度明显高于前两种。4. 配置之后如何验证确实启用成功4.1 用 crossOriginIsolated 这个“官方开关”最靠谱的验证方式是直接读取浏览器暴露的全局属性if (self.crossOriginIsolated) { console.log(跨域隔离已启用SharedArrayBuffer 可用); const sab new SharedArrayBuffer(1024); const view new Int32Array(sab); console.log(view.length); // 256 } else { console.warn(跨域隔离未启用); }crossOriginIsolated是浏览器根据 COOP、COEP、安全上下文三个条件综合计算出来的最终结果。它为 true 就说明条件都满足了SharedArrayBuffer 一定会重新出现在全局作用域里。反过来它如果为 false就算页面里某个 case 下能拿到 SharedArrayBuffer整体也不能依赖。这里要提醒一点安全上下文是硬性前提。跨域隔离只在 HTTPS 环境或者 localhost 环境下生效。如果通过http://192.168.1.10:8080访问即便响应头配置完全正确crossOriginIsolated 仍然会是 false。4.2 用 DevTools 和命令行看响应头验证响应头最直接的办法是打开 Chrome DevTools 的 Network 面板点击上方任意一个文档请求查看 Response Headers 里是否出现Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp没有的话就是没下发成功。还可以在命令行里快速确认curl -I https://your-domain.com/输出里如果能看到这两个头说明服务器配置生效。需注意到 CDN 边缘节点如果缓存了旧头你修改源站之后可能还是拿到旧值这时候需要刷新 CDN 缓存或者设置默认的兜底响应头。4.3 用一个真实场景做最终验证只看属性还不够稳妥的做法是直接让一个 Worker 用 SharedArrayBuffer 做一次内存共享读写确认业务路径真的通了。主线程代码const sab new SharedArrayBuffer(4); const arr new Int32Array(sab); arr[0] 0; const worker new Worker(/worker.js); worker.postMessage(sab); worker.onmessage (e) { console.log(Worker 写入后的结果, e.data); };worker.jsself.onmessage (e) { const arr new Int32Array(e.data); Atomics.store(arr, 0, 128); self.postMessage(arr[0]); };如果 Worker 能正常把 128 传回主线程而且没有出现SharedArrayBuffer is not defined那整条链路就是通的。这个测试建议放在应用的核心初始化流程里一旦环境异常立刻给出明确提示避免用户用到后面才发现功能失效。5. 常见问题与排查技巧实录5.1 页面图片全碎了COEP 拦截了跨域图床我自己第一次开启require-corp后最先崩的就是运营配置的图片 CDN。那些图片来自完全不同的域名加载时没带 crossorigin 属性CDN 响应也没 CORP 头结果全被 COEP 拦下来。报错长这样Refused to load the image https://cdn.example.com/a.jpg because it violates the following Content Security Policy directive: require-corp注意这里的用词虽然提到了 CSP但实际上是在说 COEP 的资源拦截。解决办法最快的是给图片标签加 crossoriginimg srchttps://cdn.example.com/a.jpg crossoriginanonymous /同时要求 CDN 返回Access-Control-Allow-Origin头。另一种方案是让运维在 CDN 上统一加Cross-Origin-Resource-Policy: cross-origin这样就不用改前端代码。5.2 window.opener 失效了登录弹窗回不去设置了 COOP: same-origin 后跨源弹窗的 opener 被切断弹窗里window.opener.xxx()会直接报 null。这在做第三方授权登录时特别常见。我一般推荐用 postMessage 替代父页面监听 message 事件弹窗用window.opener.postMessage({type: ok}, *)上报结果。如果你还依赖弹窗同步消息最好在弹窗里直接请求后端接口标记登录状态父页面轮询该接口最稳。5.3 第三方脚本突然不执行了很多第三方 SDK 的加载地址不会主动设置 CORS 头脚本标签又没有 crossoriginCOEP 一开就会把这些脚本全部拦截。表现为统计代码不生效、客服组件不出来、广告系统直接空白。有三个应对方向把用到的第三方脚本改成通过同源代理加载代理返回带 CORP 头。给 script 标签加 crossorigin前提是第三方服务器支持 CORS。把 COEP 切到 credentialless接受跨源请求不带凭据的代价。如果业务无法接受 credentialless又确实要加载大量第三方脚本那么开发成本最大的方向是全面梳理所有第三方资源确认各自能不能补 CORP/CORS必要时用 Service Worker 兜底。5.4 跨域 iframe 被拦截涉及支付页面、地图组件、视频播放器等跨域 iframe 时COEP 同样会要求 iframe 的响应带上 CORP 头。这类页面往往不能由你控制问题非常棘手。常见的做法是给 iframe 的 src 做一个同源包装页或者干脆把目标站点放在子域名用same-site级别的 CORP 处理。这里必须做单独的兼容测试不能想当然。5.5 排查清单速查表现象可能原因处理方式crossOriginIsolated 为 falseCOOP/COEP 缺一个或页面非 HTTPS/localhost确认响应头完整且页面在安全上下文图片/脚本被拦截跨源资源没有 CORP/CORS加 CORP 头、加 crossorigin 属性或改用 credentiallesswindow.open 拿不到 openerCOOP 切断了跨源窗口引用用 postMessage 或后端接口代替本地正常线上失败代理、CDN 没有透传响应头检查 CDN 配置和源站是否一致错误页/重定向没有头Nginx add_header 没加 always补上 always 并检查继承覆盖页面在 iframe 里被嵌被嵌页面本身需要隔离嵌入方也受影响单独评估嵌入场景必要时对 iframe 场景降级5.6 灰度与降级方案跨域隔离不是加个响应头就能无感上线的。最大的风险在于 COEP 拦截资源后页面直接破相而用户往往不会看控制台报错只会觉得“网站坏了”。我现在的习惯是分三步走先在测试环境把跨境资源全部梳理一遍列出一个“受 COEP 影响的资源清单”然后在一小部分流量上灰度用 Reporting API 收集异常资源等确认拦截数量在可接受范围后再全量放开。如果项目不允许这么细的灰度直接切到credentialless会平滑很多代价是跨源请求不再带 Cookie需要评估业务是否有依赖。5.7 无第三方资源的简单项目配置参考如果你的项目是完全自控资源的纯前端应用推荐直接上全套Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp Cross-Origin-Resource-Policy: cross-origin第一个头保护窗口隔离第二个头强制所有跨源资源都要声明许可第三个头告诉其他站点这里的静态资源允许被跨源加载。三者配合既满足 SharedArrayBuffer 的启用条件也不会拦截同域静态资源遇到跨域 CDN 只要 CDN 把 CORP 配上就能跑。配置跨域隔离和 SharedArrayBuffer 的整个过程本质上是一次“用安全约束换取性能特性”的等价交换。你多做几个响应头的配置解决的是多线程共享内存的可用性问题但这个配置会让你的页面在嵌入第三方资源时变得比较挑剔。我的体会是动手之前先盘一遍现有资源再决定用 require-corp 还是 credentialless否则很容易在改完之后被各种拦截报错折腾得怀疑人生。