我的页面要嵌一个第三方网站的 iframe,结果 iframe 里的内容死活不出来------白屏。查了半天,发现不是我的问题,是对方的 CSP 把我拦在门外了 :
frame-ancestors白名单里没有我的域名,浏览器直接拒绝加载。这让我重新理解了跨域和 CSP 的关系------跨域是浏览器的门禁,CSP 是对方自己加的防盗门,两道门你都得过。这篇文章从我踩的坑出发,把跨域和 CSP 讲透。
一、先搞清楚:跨域是啥?为什么要有?
一个比喻:小区门禁
浏览器就是一个小区,每个网站是一个住户。
- 同源 = 同一个住户(屋内随便走)
- 跨域 = 不同住户(串门得敲门)
同源策略就是小区门禁------不同住户之间,不能随便进对方家门、翻对方冰箱、拿对方东西。
没有门禁会怎样?任何住户都能进你家拿 Cookie、改你的页面、甚至冒充你给银行发请求。那这个小区谁还敢住?
什么算"同源"?
三个要素全一样才算同源,只要有一个不同就是跨域:
bash
https://example.com/page1
https://example.com/page2 ✅ 同源
http://example.com vs https://example.com ❌ 协议不同
https://a.com vs https://b.com ❌ 域名不同
https://example.com vs https://example.com:8080 ❌ 端口不同
https://sub.example.com vs https://example.com ❌ 子域名也算不同源!
二、iframe 跨域:两道门,都得过
我的场景是这样的:
arduino
我的页面:https://myapp.com
要嵌的页面:https://partner-site.com
html
<!-- 我的页面里 -->
<iframe src="https://partner-site.com/dashboard"></iframe>
结果------白屏。iframe 里啥也没有,控制台报了个错:
arduino
Refused to display 'https://partner-site.com/' in a frame because
it set 'X-Frame-Options' to 'deny' and/or 'Content-Security-Policy'
to "frame-ancestors 'none'".
翻译成人话:对方说"我不允许被嵌入 iframe",浏览器照办了。
这就是我遇到的场景。表面上看是"跨域问题",但实际不是同源策略拦的------同源策略只管"能不能操作 iframe 里的 DOM",不管"能不能加载 iframe"。拦我的是对方的 CSP,一道更严的门。
理解这个区别很关键:
| 被什么拦了 | 现象 | 谁设的规则 |
|---|---|---|
| 同源策略 | 能加载 iframe,但 JS 读不到 iframe 内的 DOM | 浏览器内置,改不了 |
| CSP frame-ancestors | iframe 直接白屏,内容都不加载 | 对方网站自己设的,可以改 |
同源策略是小区门禁,CSP 是住户自己装的防盗门。门禁你绕不过去,但防盗门对方可以把你加到白名单里。
三、CSP 的 frame-ancestors:谁能把我嵌到 iframe 里?
frame-ancestors 是 CSP 里专门管 iframe 嵌入的指令。它告诉浏览器:我只允许这些网站把我放到 iframe 里,其他人一概拒绝。
它有三个档位
html
<!-- 档位一:谁都不许嵌(最严) -->
<meta http-equiv="Content-Security-Policy" content="frame-ancestors 'none'" />
<!-- 档位二:只有自己能嵌 -->
<meta http-equiv="Content-Security-Policy" content="frame-ancestors 'self'" />
<!-- 档位三:白名单,允许指定的网站嵌 -->
<meta
http-equiv="Content-Security-Policy"
content="frame-ancestors 'self' https://myapp.com"
/>
| 设值 | 效果 | 类比 |
|---|---|---|
'none' |
任何网站都不能 iframe 嵌入 | 谁都不让进门 |
'self' |
只有同源页面能 iframe 嵌入 | 只让家里人进 |
'self' https://myapp.com |
同源 + 指定白名单域名能嵌入 | 家人 + 登记访客能进 |
我的问题出在哪?
partner-site.com 的 CSP 设了 frame-ancestors 'self'(只允许自己嵌自己),而我的 myapp.com 不在白名单里 → 浏览器直接拒绝加载 iframe。
解法很简单 ------让 partner-site.com 把我的域名加到白名单里:
html
<!-- partner-site.com 的 CSP 改成 -->
<meta
http-equiv="Content-Security-Policy"
content="frame-ancestors 'self' https://myapp.com"
/>
或者通过 HTTP 响应头(更推荐,因为 meta 标签有些浏览器处理有延迟):
less
Content-Security-Policy: frame-ancestors 'self' https://myapp.com;
改完之后,iframe 就能正常加载了。
四、为什么 frame-ancestors 默认这么严?------防点击劫持
你可能会问:凭什么对方要限制我嵌 iframe?我就想展示一下他的页面,又没恶意。
因为有一种攻击叫点击劫持(Clickjacking):
arduino
攻击者的页面 evil.com:
┌─────────────────────────────────────┐
│ "点击领 100 元红包" │ ← 用户看到的是这个按钮
│ ┌─────────────────────────────────┐│
│ │ 透明 iframe: bank.com/transfer ││ ← 实际是透明的银行转账页面
│ │ [确认转账] ← 正好对准红包按钮 ││
│ └─────────────────────────────────┘│
└─────────────────────────────────────┘
用户以为点的是"领红包",实际点的是银行页面的"确认转账"!
原理 :攻击者在自己的页面上覆盖一个透明的 iframe,iframe 里加载银行/社交/支付网站,把按钮对准到攻击者想让你点的位置。你看到的和点到的,完全是两个东西。
frame-ancestors 就是为了防这个------如果银行网站设了 frame-ancestors 'none',那 evil.com 就没法把它嵌到 iframe 里,点击劫持直接失效。
所以对方不是在针对你,是在保护自己的用户。你得让对方信任你,才把你加到白名单。
五、frame-ancestors 和 X-Frame-Options 的关系
你可能还见过一个叫 X-Frame-Options 的响应头:
makefile
X-Frame-Options: DENY ← 谁都不许嵌
X-Frame-Options: SAMEORIGIN ← 只允许同源嵌
X-Frame-Options: ALLOW-FROM https://myapp.com ← 允许指定域名(已废弃)
这是 CSP 之前的老方案,现在的关系是:
X-Frame-Options |
frame-ancestors |
|
|---|---|---|
| 年代 | 2009 年,IE8 时代 | 2013 年,CSP Level 2 |
| 支持白名单 | ❌ ALLOW-FROM 已废弃 |
✅ 任意多个域名 |
| 功能 | 只管 iframe 嵌入 | CSP 大体系的一部分 |
| 优先级 | 两者都有时,frame-ancestors 优先 |
--- |
最佳实践:两个都设上,兼容老浏览器:
less
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self' https://myapp.com;
如果只有 frame-ancestors,现代浏览器够用。但如果要兼容 IE,还得加 X-Frame-Options。
六、iframe 加载的三道关卡
现在我把 iframe 加载可能遇到的所有拦截列出来,帮你一次搞清楚:
ini
你的页面 myapp.com 嵌 <iframe src="https://partner-site.com">
│
┌─────▼──────┐
│ 第一道关卡 │ CSP frame-ancestors
│ 对方允许吗?│ partner-site.com 的 CSP 白名单里有没有 myapp.com?
└─────┬──────┘
│ ✅ 通过
┌─────▼──────┐
│ 第二道关卡 │ X-Frame-Options
│ 老版本允许?│ partner-site.com 有没有设 DENY?
└─────┬──────┘
│ ✅ 通过
┌─────▼──────┐
│ 第三道关卡 │ 同源策略
│ 能操作 DOM?│ 你的 JS 能不能读 iframe 里的内容?
└─────┬──────┘
│
┌────┴─────┐
│ │
✅ 同源 ❌ 跨域
可操作DOM 只能展示,不能读
关键区分:
- 第一、二道关卡没过 → iframe 白屏,内容都不加载
- 第三道关卡没过 → iframe 正常显示,但 JS 读不到里面内容
我的场景是第一道关卡没过------frame-ancestors 白名单没我。
七、说到跨域,必须提 CORS
iframe 被拦是一种跨域问题,还有一种更常见的是请求被拦 ------用 fetch/XHR 调另一个域名的 API,浏览器拦截了响应。
CORS 是什么?
Cross-Origin Resource Sharing(跨域资源共享)。
如果同源策略是小区门禁,那 CORS 就是物业发的访客证------服务端在响应头里声明"我允许谁来拿我的数据",浏览器看到访客证就放行。
js
// 前端请求
fetch("https://api.partner-site.com/data");
yaml
# 服务端响应头
Access-Control-Allow-Origin: https://myapp.com ← 告诉浏览器:这个域可以拿
Access-Control-Allow-Methods: GET, POST ← 允许哪些方法
Access-Control-Allow-Credentials: true ← 允许带 Cookie
CORS 和 CSP 的区别
css
CORS:我的数据,谁可以来拿? → 服务端说了算
CSP:我的页面,只接受谁的资源? → 页面自己说了算
frame-ancestors:我的页面,谁可以 iframe 嵌入? → 页面自己说了算
CSP frame-ancestors 管的是"能不能嵌入",CORS 管的是"能不能拿数据"。两个独立机制,互不影响。
| 管什么 | 谁设的 | 场景 | |
|---|---|---|---|
| 同源策略 | DOM 操作/数据读取 | 浏览器内置 | 父页面读 iframe 的 DOM |
| CORS | 跨域请求响应 | 被请求方服务端 | fetch 调第三方 API |
| CSP frame-ancestors | iframe 能不能被嵌入 | 被嵌入方页面 | iframe 白屏 |
八、CSP 不只是 frame-ancestors------它是一整套安全体系
frame-ancestors 只是 CSP 十几个指令之一。CSP 是浏览器的安全护盾,从多个维度保护你的页面:
核心指令速览
html
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self'; ← 所有资源默认只允许同源
script-src 'self' 'nonce-abc123'; ← JS 只允许同源 + 特定 nonce
style-src 'self' 'unsafe-inline'; ← CSS 允许同源 + 内联
img-src 'self' data: https:; ← 图片允许同源 + data:URL + HTTPS
font-src 'self' https://fonts.gstatic.com; ← 字体允许同源 + Google Fonts
connect-src 'self' https://api.example.com; ← 请求只允许发到同源 + 指定API
frame-ancestors 'self' https://myapp.com; ← iframe 只允许同源 + 指定域名嵌入
object-src 'none'; ← 禁止 Flash/object
base-uri 'self'; ← 防篡改 <base href>
form-action 'self'; ← 表单只提交到同源
upgrade-insecure-requests; ← HTTP 自动升级 HTTPS
"
/>
CSP 的好处
| 好处 | 对应指令 | 没有会怎样 |
|---|---|---|
| 防 XSS 注入 | script-src |
注入的 <script> 执行,Cookie 被偷 |
| 防点击劫持 | frame-ancestors |
被透明 iframe 覆盖,用户被骗点击 |
| 防数据外泄 | connect-src |
恶意 JS 把数据 fetch 到攻击者服务器 |
| 防资源注入 | img-src/font-src |
追踪像素、恶意字体加载 |
| 强制 HTTPS | upgrade-insecure-requests |
混合内容漏洞 |
| 防表单劫持 | form-action |
表单被偷偷提交到钓鱼站 |
| 防基础路径篡改 | base-uri |
改 <base href> 把所有相对 URL 劫持 |
CSP 会导致什么问题?
安全是有代价的------CSP 等于给自己加了一堆门禁,方便性和安全性永远矛盾:
| 问题 | 现象 | 原因 |
|---|---|---|
| 内联脚本全废 | onclick="handle()" 不执行 |
script-src 不允许内联,得用 nonce |
| eval 不能用 | eval() / new Function() 报错 |
unsafe-eval 默认禁用 |
| CDN 加载失败 | React 等从 CDN 加载报错 | 白名单没加 CDN 域名 |
| 第三方嵌入被拦 | Google Analytics / 广告脚本不加载 | 域名不在白名单 |
| 开发体验下降 | 每加新资源都得改 CSP | 白名单是死的,不在名单全拦 |
正确上 CSP 的姿势 :先用 Content-Security-Policy-Report-Only(只记录不拦截),观察一周确认没误拦,再改成真正的 CSP。
九、回到我的场景:最终怎么解决的?
bash
我的页面:https://myapp.com
对方页面:https://partner-site.com ← CSP 设了 frame-ancestors 'self'
我需要让对方把我加到白名单里。步骤:
第一步:确认是 CSP 拦的
F12 控制台看到:
arduino
Refused to display 'https://partner-site.com/' in a frame because
it set 'Content-Security-Policy' to "frame-ancestors 'self'".
第二步:联系对方,让他们加白名单
对方服务端的响应头从:
css
Content-Security-Policy: frame-ancestors 'self';
改成:
less
Content-Security-Policy: frame-ancestors 'self' https://myapp.com;
第三步:确认生效
刷新页面,iframe 正常加载。搞定。
如果对方不改怎么办?
如果对方拒绝加白名单(安全策略不允许),那就没法 iframe 嵌入了------这是 CSP 的设计意图,它就是让网站有权拒绝被嵌入。你只能换方案:
- 新窗口打开 :
<a href="https://partner-site.com" target="_blank">------ 不走 iframe,不受frame-ancestors限制 - 后端代理:服务端抓取对方页面内容,渲染到自己的模板里------但这可能违反对方的使用条款
- 让对方提供 API:不走 iframe,用 API 拿数据自己渲染------最正规的方案
十、一张图总结
css
跨域问题
┌┴┴┴┴┴┐
│ │
┌────────┘ └────────┐
▼ ▼
iframe 跨域 请求跨域
│ │
│ ┌────┴────┐
│ ▼ ▼
│ 简单请求 非简单请求
│ 直接发 先预检(OPTIONS)
│ 再正式发
│ 服务端设 CORS 头
│
▼
iframe 加载有三道关卡
│
├─❶ CSP frame-ancestors → 对方允许你嵌吗?
│ 没过 → 白屏,内容都不加载
│ 解法:让对方把你加白名单
│
├─❷ X-Frame-Options → 老版本允许吗?
│ 没过 → 白屏
│ 解法:同上,改对方响应头
│
└─❸ 同源策略 → 能操作 iframe 里的 DOM 吗?
同源 → 能操作
跨源 → 只能展示,不能读
解法:postMessage 通信
另外还有 CSP 这套安全体系:
script-src → 防 XSS
frame-ancestors → 防点击劫持
connect-src → 防数据外泄
form-action → 防表单劫持
...
一句话总结:同源策略是浏览器的门禁,CORS 是对方发的访客证,CSP 是对方自己装的防盗门。我的 iframe 白屏,不是门禁拦的,是防盗门拦的------对方得把你加到白名单里才让你进。