一、跨域隔离要解决的到底是什么问题
浏览器里的同源策略是一道很粗的墙:不同源之间的脚本不能读彼此的 DOM、不能读对方的响应体。但有些攻击不需要读响应体,只需要「让请求发出去」或者「知道请求成功了没有」。最典型的是 Spectre 类侧信道攻击——攻击者通过测量内存访问时间,推断出另一个源的内存内容。这类攻击绕过了同源策略的全部假设,因为同源策略管的是「访问」,不是「执行」。于是浏览器厂商引入了另一层防护:跨域隔离(Cross-Origin Isolation)。
跨域隔离不是让页面更安全这种模糊的说法,它有非常明确的三个前提条件,全部满足才能启用一批高精度计时和高性能 API:页面必须处于「跨源隔离」状态,也就是 window.crossOriginIsolated === true。要达成这个状态,需要同时满足两个响应头:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corpCOOP 负责隔离浏览上下文组,让跨源窗口之间失去 window.opener 引用、无法通过 postMessage 随意通信。COEP 负责隔离子资源,要求页面加载的每一个跨源资源都必须显式声明允许被嵌入。二者合起来,保证页面的整个执行环境(包括所有子资源)都在同一个源的控制之下。
满足条件之后能拿到什么?最关键的是 SharedArrayBuffer,因为跨线程共享内存是 Spectre 攻击的核心工具。其次是更精确的 performance.now() 精度(从被模糊化的 100 微秒级恢复到 5 微秒级),以及一些高性能图形与计算 API。如果只是想做一个普通的资讯站,你很可能一辈子用不到这些。
二、为什么大部分站长应该慎重
这里有个很常见的误判:看到安全建议里说「开启 COOP 和 COEP 能提高安全性」,于是直接加进 Nginx 配置,结果页面白屏或者样式全乱。原因是 COEP 的 require-corp 模式非常严格,它不是「建议」而是「强制」:任何没有明确授权的跨源资源都会被浏览器拒绝加载。
会被拦下的典型资源包括:第三方 CDN 上的 jQuery、Google Fonts 的 CSS 与字体、外链的广告脚本、内嵌的第三方视频播放器、微信/微博的分享组件、统计脚本里动态插入的 iframe。这些服务商大多没有适配 COEP,它们的响应里不带 Cross-Origin-Resource-Policy 头,于是全部被拒。
更麻烦的是排查成本。COEP 拦截在控制台里只报一条 net::ERR_BLOCKED_BY_RESPONSE,页面不会崩溃,但会「看起来还行、功能少一半」——某个按钮点了没反应,某张图不显示。这种半坏状态比彻底白屏更难发现。
所以第一件事不是改配置,而是问自己:我的站需不需要 SharedArrayBuffer?如果答案是不需要,那就不要开 COEP。COOP 可以单独开(它只影响弹窗与 opener 关系),风险小得多,但也要注意它会让「第三方登录弹窗回传 token」这类流程失效。
三、只想要 COOP:低风险的一步
如果你的站没有依赖弹窗回传的登录流程,单独启用 COOP 是相对安全的:
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Opener-Policy "same-origin-allow-popups" always;这两个值不能同时生效,只能选一个。same-origin 最严格,任何跨源弹出窗口都会失去与打开者的引用关系。same-origin-allow-popups 则允许页面自己打开的弹窗保持 opener 关系,适合「点按钮打开第三方授权页、用 postMessage 回传」的场景,代价是这种弹窗仍能通过 opener 反向操作你的页面,防护强度略低。
选择依据很实际:你的站有没有 OAuth 登录弹窗、有没有第三方客服挂件会弹出新窗口。有就选后者,没有就选前者。改完要专门测一遍:打开一个外链(比如友情链接),确认 target="_blank" 的页面能正常打开,且新页面里的 window.opener 是 null。如果新页面的脚本因为拿不到 opener 而报错,说明它依赖这个引用,需要评估影响。
又一个细节:add_header 在 Nginx 里有个著名的行为——只要内层 location 里出现了任何 add_header,外层的所有 add_header 都会被丢弃。很多站点在静态资源 location 里加了 Cache-Control,结果主配置里的安全头在那些路径上全部失效。解决办法是用 always 参数(它只影响错误响应,不解决继承问题),或者把安全头集中到一个 include 文件,在每个带 add_header 的 location 里都 include 一遍。这一点值得单独检查,因为它会静默地让「我以为加了」变成「其实没加」。
四、真的要开 COEP:先做资源盘点
如果确实需要 SharedArrayBuffer(比如做在线视频剪辑、WebAssembly 重型计算、端侧模型推理),那就得开 COEP,而且必须先把资源盘点清楚。
盘点的方法是打开页面,在控制台里把所有被 COEP 拦下的请求列出来。可以直接用脚本统计:
performance.getEntriesByType('resource')
.map(function(r) { return r.name; })
.filter(function(n) { return n.indexOf(location.origin) !== 0; })
.filter(function(n, i, a) { return a.indexOf(n) === i; });这段代码给出页面上所有跨源资源。注意它只能拿到「成功加载」的,被拦下的不会出现在 getEntriesByType 里,所以更可靠的做法是先在测试环境开启 COEP,然后收集控制台错误,或者用 report-to 上报。
盘点结果通常分三类,处理方式完全不同:
| 资源类型 | 典型例子 | 处理 |
|---|---|---|
| 自己域名下的静态资源 | CSS、JS、图片、字体 | 无需处理,同源天然通过 |
| 支持 CORP 的第三方 | 部分大厂 CDN、部分字体服务 | 加 crossorigin 属性即可 |
| 不支持 CORP 的第三方 | 老 CDN、广告脚本、统计代码 | 无法适配,只能下架或代理回源 |
第三类是真正的麻烦。可行的路径只有三条:一是把这些资源复制到自己域名下托管(字体尤其适合,还能顺带解决国内字体 CDN 不稳定);二是用服务端反向代理,让它们变成同源请求(要注意版权与授权条款);三是干脆不用这个资源。
五、CORP 与 CORS 的分工
开启 COEP 之后,跨源资源要么带 Cross-Origin-Resource-Policy 响应头,要么走 CORS 流程。这两个机制经常被混淆,需要说清楚。
Cross-Origin-Resource-Policy 由被请求方在响应头里声明,取值有三个:same-origin(只允许同源引用)、same-site(允许同站,比如 a.example.com 引用 b.example.com)、cross-origin(允许任何站点引用)。它是「资源方主动声明可以被谁用」,不需要请求方做任何事。
CORS 则是请求方发起的协商:请求方带 Origin 头,响应方回 Access-Control-Allow-Origin。在 COEP 环境下,如果资源带了正确的 CORS 头,也算通过——但前提是引用它的元素上要有 crossorigin 属性,否则浏览器按 no-cors 模式请求,拿不到 CORS 授权。
<img src="https://cdn.example.net/a.jpg" crossorigin="anonymous">
<script src="https://cdn.example.net/lib.js" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.example.net/a.css" crossorigin="anonymous">注意 crossorigin="anonymous" 与 crossorigin="use-credentials" 的区别:前者不带 cookie,后者的响应必须回 Access-Control-Allow-Credentials: true 且 Allow-Origin 不能是通配符。字体资源总是走 CORS(这是 CSS 字体规范的要求),所以字体最容易在 COEP 下出问题。
六、给自己站点加 CORP 头
反过来,如果你的站提供资源给别人的页面用(比如你有一个公开的 JS 库、一套图片 CDN),可以主动加 CORP 头,让使用你资源的站点能开 COEP:
location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?)$ {
add_header Cross-Origin-Resource-Policy "cross-origin" always;
add_header Access-Control-Allow-Origin "*" always;
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
}这里要注意前面提到的 add_header 继承问题:这个 location 里出现了三个 add_header,主配置里的 COOP、COEP、X-Content-Type-Options 都不会继承过来。所以如果主站也开了 COEP,必须在这个 location 里把它们补一遍。这是配置类问题里最典型的「看起来配了、实际没生效」。
对于不想让外人引用的资源(比如用户上传的图片),反而应该加 same-origin,防止被别的站热链或者在 COEP 环境下被滥用。这里要区分「防止盗链」和「CORP」——前者是省钱,后者是安全,但副作用是同一个:别的站引用你的图会失败。
七、用报告模式先观察再上线
COEP 支持 report-only 模式,只上报不拦截,这是最稳妥的上线方式:
add_header Cross-Origin-Embedder-Policy-Report-Only "require-corp" always;
add_header Cross-Origin-Opener-Policy-Report-Only "same-origin" always;
add_header Reporting-Endpoints 'default="https://www.example.com/csp-report"' always;上报接收端是一个你自己的接口,收到的 JSON 里有 blockedURL、type、disposition 等字段。部署后观察一周,统计 blockedURL 的域名分布,就知道还有多少资源没适配。
判断是否可以正式开启的标准要具体,不能是「看着差不多了」。建议是:连续七天 disposition: enforce 的上报数为零,且在这一周里覆盖了主要页面模板(首页、文章页、搜索结果页、用户中心)。如果某个模块一周没被访问过,报告模式自然也报不出它的问题,正式开启后就可能踩到。
收到报告后不要立刻加白名单——白名单在 COEP 里不存在。唯一的处理是让资源方加头,或者自己代理。如果有人建议「加个 iframe 绕一下」,那等于放弃了隔离的意义,不如不开。
八、常见故障与处置
开了 COEP 后图片全部裂开:八成是图片走的是外部图床。图床一般不给 CORP 头,唯一的办法是迁移到自己域名。如果你用了对象存储,可以在存储桶的跨域设置里加 CORP,云厂商的控制台一般都有这一项。
开了 COEP 后广告位变空白:AdSense 等广告脚本通常不支持 COEP。这不是配置问题,是产品能力问题。这类站点就不该开 COEP,除非你能接受放弃广告收入。
crossOriginIsolated 一直是 false:先确认两个头都真的下发了。用 curl -sI 看最终响应,注意要跟随重定向(-L),因为有些站首页会 301,而头只加在了某一层。另一个常见原因是页面里嵌了一个不带 COEP 的 iframe——iframe 的响应也必须带 COEP,否则整个页面失去隔离状态。这个问题最隐蔽,因为主文档的头完全正确。
开了 COOP 后第三方登录回不来:回到 same-origin-allow-popups,或者把登录流程改成整页跳转而不是弹窗。后者其实更安全,也更好做移动端适配。
九、小结
跨域隔离是一把针对性很强的工具,它保护的是「页面执行环境不被侧信道窥探」,而不是「网站不被攻击」。对绝大多数内容站、博客、企业站来说,它带来的收益接近于零,而代价是一整套第三方资源的适配工作。
真正值得做的是三件成本极低的事:单独加 COOP(无弹窗依赖时)、给自建静态资源加 CORP 头(让别人能安全引用你)、以及用 report-only 模式观察一段时间再决定。判断标准也很清晰:如果你在代码里搜不到 SharedArrayBuffer,也没有需要高精度计时的功能,那就不开 COEP。安全配置的第一原则是「为了具体威胁而存在」,没有威胁的配置只会变成未来的故障源。