我在自家页面嵌了个 iframe,结果对方说"你不配"——跨域和 CSP 的那些坑

我的页面要嵌一个第三方网站的 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-ancestorsX-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 的设计意图,它就是让网站有权拒绝被嵌入。你只能换方案:

  1. 新窗口打开<a href="https://partner-site.com" target="_blank"> ------ 不走 iframe,不受 frame-ancestors 限制
  2. 后端代理:服务端抓取对方页面内容,渲染到自己的模板里------但这可能违反对方的使用条款
  3. 让对方提供 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 白屏,不是门禁拦的,是防盗门拦的------对方得把你加到白名单里才让你进。

相关推荐
百珏8 小时前
海量人群包存储优化:基于 RoaringBitmap 交换格式与 Redis 分片 Bitmap 的实践
java·后端·架构
Awu12278 小时前
🍎Google Stitch :用自然语言做 UI 设计,把设计师的活也抢了
前端·aigc·ai编程
竹林8188 小时前
从“连接不上”到“交易成功”:我用 @solana/web3.js 在 React 中搞定 Solana 钱包交互的全过程
前端
heimeiyingwang8 小时前
【架构实战】安全性设计:让系统固若金汤
架构
YouCanYouUp.9 小时前
掌控感心理学解析:人类最底层的心理需求
前端
wyc是xxs9 小时前
浏览器解析HTML头部的底层逻辑
前端·html
义嘉泰9 小时前
一颗 NAND Flash 的自我修养
前端·人工智能·芯片
liangdabiao9 小时前
【开源】利用Claude Agent SDK能力通过Skill自主构建完整的web
前端·开源
张元清9 小时前
驯服 React 里的 DOM 事件:useEventListener、useEventEmitter、useKeyModifier、useTextSelect
前端·javascript·面试