出处:掘金
原作者:尽欢 i
什么是 iframe?
iframe(inline frame)翻译过来叫内联框架,是一种在 HTML 页面中嵌入另一个 HTML 页面的方法。通过 iframe,可以在当前网页中显示另一个完全独立的网页,它们是彼此分离的。换句话说,iframe 允许在当前页面中创建一个子窗口,而该窗口可以加载另一个网站或内容。
iframe 的常见用途:
- 嵌入视频:如 YouTube、Vimeo 等视频平台提供的嵌入代码;
- 加载广告:广告常常通过 iframe 嵌入,以确保广告内容与主页面隔离;
- 嵌入外部内容:将一个完全独立的页面嵌入到当前页面中,比如加载第三方应用、表单等;
- 内嵌文档:将 PDF、HTML、网站等文件展示在网页中;
- 跨域内容嵌入:通过 iframe 可以显示来自不同域名的内容,但需要考虑一些安全限制
无论是集成第三方服务、实现微前端架构,还是构建安全的沙盒环境,iframe 都以其独特的隔离机制和跨域能力成为开发者的首选方案。虽然 iframe 能快速解决跨页面内容嵌套问题,但也成为性能瓶颈或安全漏洞的源头。
iframe 常见属性
1. 核心属性
属性 | 说明 | 示例 |
---|---|---|
src | 指定要嵌入的页面 URL(必需) | <iframe src="https://example.com"></iframe> |
width | 设置 iframe 的宽度(像素或百分比) | <iframe width="600"> 或 <iframe width="100%"> |
height | 设置 iframe 的高度(像素或百分比) | <iframe height="400"> |
name | 为 iframe 指定名称,可用于 <a target="iframe-name"> 的跳转 | <iframe name="myFrame"> |
title | 提供 iframe 的可访问性描述(对屏幕阅读器重要) | <iframe title="Embedded YouTube Video"> |
2. 安全与权限控制
属性 | 说明 | 示例 |
---|---|---|
sandbox | 限制 iframe 的行为(增强安全性) | <iframe sandbox="allow-scripts allow-forms"> |
allow | 控制 iframe 的特殊功能(如全屏、摄像头访问) | <iframe allow="fullscreen; camera"> |
allowfullscreen | 允许 iframe 内容进入全屏模式(如视频) | <iframe allowfullscreen> |
referrerpolicy | 控制 iframe 请求的 Referer 头 | <iframe referrerpolicy="no-referrer"> |
loading | 控制 iframe 的加载方式(eager 或 lazy ) | <iframe loading="lazy"> |
3. 样式与布局
属性 | 说明 | 示例 |
---|---|---|
style | 内联 CSS 样式 | <iframe style="border: none;"> |
4. 跨域与通信
属性 | 说明 | 示例 |
---|---|---|
srcdoc | 直接嵌入 HTML 代码(而不是外部 URL) | <iframe srcdoc="<h1>Hello</h1>"> |
csp | 为 iframe 内容设置内容安全策略 | <iframe csp="script-src 'self'"> |
5. 现代浏览器支持的属性
属性 | 说明 | 示例 |
---|---|---|
importance | 控制资源加载优先级(high / low ) | <iframe importance="high"> |
fetchpriority | 类似 importance ,但更精细 | <iframe fetchpriority="high"> |
iframe 常见事件
1. 加载状态事件
load
- iframe 内容加载完成
iframe.addEventListener('load', () => {
console.log('iframe 内容已加载完成');
});
用途:
- 检测嵌入页面是否加载成功
- 在内容就绪后执行操作(如调整高度)
error
- iframe 加载失败
iframe.addEventListener('error', () => {
console.error('iframe 加载失败');
});
用途:
- 处理第三方页面加载失败的情况
- 显示备用内容(降级方案)
2. iframe 视口大小事件(仅同源)
仅限同源 iframe,跨域需通过 postMessage 间接实现
resize
- iframe 视口大小变化
iframe.contentWindow.addEventListener('resize', () => {
console.log('iframe 内部窗口大小变化');
});
这段代码监听的是 ==iframe 内部窗口(即子页面的 window
对象)的视口(viewport)宽度尺寸变化==,而不是 iframe 元素本身的边框尺寸或子页面内容的实际高度变化。
用途:
- 响应式布局调整
- 动态计算 iframe 内容高度
3. 跨域通信事件
message
- 跨域数据接收
// 父页面发送消息
iframe.contentWindow.postMessage('Hello', 'https://child-domain.com');
// 子页面接收
window.addEventListener('message', (event) => {
if (event.origin === 'https://parent-domain.com') {
console.log('收到消息:', event.data);
}
});
关键参数:
event.data
:传递的数据event.origin
:消息来源(必须验证!)event.source
:发送消息的窗口引用
4. 安全相关事件
securitypolicyviolation
- CSP 违规
document.addEventListener('securitypolicyviolation', (e) => {
console.warn('CSP 违规:', e.blockedURI);
});
用途:
- 监控 iframe 内的资源加载是否违反 CSP 规则
5. 用户交互事件(仅同源)
仅限同源 iframe,跨域需通过 postMessage 间接实现
mouseover
/ click
- 穿透事件监听
// 父页面捕获 iframe 内的鼠标事件(需同源)
iframe.contentDocument.addEventListener('click', (e) => {
console.log('点击了 iframe 内部元素');
});
6. 生命周期事件(微前端场景)
beforeunload
- 子页面即将卸载
// 子页面中
window.addEventListener('beforeunload', () => {
window.parent.postMessage('子页面即将关闭', '*');
});
unload
- 子页面已卸载
iframe.addEventListener('unload', () => {
console.log('iframe 内容已卸载');
});
sandbox 属性详解
sandbox:限制 iframe 内内容的权限,比如禁止脚本执行、表单提交等。常见值包括:
allow-scripts
:允许脚本运行;allow-forms
:允许表单提交;allow-same-origin
:允许 iframe 内的内容被认为与父页面同源(这非常危险,需谨慎使用)。
1. 同源 iframe(无 sandbox 属性)
如果 <iframe>
的 src
与父页面同源(协议、域名、端口相同):
- 完全访问权限
- iframe 内的 JavaScript 可以操作父页面的 DOM(如
parent.document
)。 - 父页面的 JavaScript 也可以操作 iframe 的 DOM(如
iframe.contentDocument
)。
- iframe 内的 JavaScript 可以操作父页面的 DOM(如
- 无额外限制
- 可以执行脚本、提交表单、弹窗、加载插件等。
- 可以访问
localStorage
、Cookie
等存储。
- 典型用途
- 微前端架构(同源子应用)。
- 模块化页面拆分。
<!-- 父页面:https://example.com/parent.html -->
<iframe src="https://example.com/child.html"></iframe>
<!-- child.html 可以完全访问 parent.html 的 DOM -->
<script>
window.parent.document.body.style.backgroundColor = "red";
</script>
2. 跨域 iframe(无 sandbox 属性)
如果 <iframe>
的 src
与父页面不同源:
- 受同源策略限制
- iframe 内的 JavaScript 无法访问父页面的 DOM(
parent.document
会报错)。 - 父页面的 JavaScript 无法访问 iframe 的 DOM(
iframe.contentDocument
会报错)。
- iframe 内的 JavaScript 无法访问父页面的 DOM(
- 但仍可能带来风险
- iframe 可以自由执行脚本、弹窗、提交表单(可通过csp限制)。
- 可能被用于点击劫持(Clickjacking)攻击。
<!-- 父页面:https://example.com -->
<iframe src="https://malicious-site.com"></iframe>
<!-- malicious-site.com 虽然不能访问父页面,但可以诱导用户点击 -->
<style>
iframe {
opacity: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
3. 带有 sandbox 属性
如果只写 sandbox
而不指定任何允许的权限,此时 iframe 内的网页将受到以下限制(无论是否同源):
受限功能 | 说明 |
---|---|
JavaScript 执行 | 所有脚本(内联和外部)都会被禁用 |
表单提交 | 无法提交表单 |
弹出窗口 | 禁止 window.open() 或 target="_blank" 等行为 |
插件加载 | 禁止加载 Flash、PDF 等插件 |
同源访问 | 即使 iframe 的 src 是同源的,也会被视为不同源(无法访问父页面的 DOM) |
自动播放媒体 | 禁止视频/音频自动播放 |
Cookie/Storage | 无法使用 localStorage 、sessionStorage 、Cookie |
AJAX/Fetch 请求 | 禁止发送网络请求 |
可以通过 sandbox="allow-xxx"
来按需启用特定功能,例如:
<!-- 允许脚本执行和表单提交 -->
<iframe src="..." sandbox="allow-scripts allow-forms"></iframe>
常用 allow-
选项:
选项 | 作用 |
---|---|
allow-scripts | 允许执行 JavaScript(但仍不能弹窗或访问父页面) |
allow-forms | 允许提交表单 |
allow-same-origin | 让 iframe 被视为同源(可以访问父页面的 DOM,但需谨慎使用!) |
allow-popups | 允许 window.open() 或 <a target="_blank"> 打开新窗口 |
allow-modals | 允许弹窗(如 alert() 、confirm() ) |
allow-orientation-lock | 允许锁定屏幕方向(适用于移动端) |
allow-pointer-lock | 允许鼠标指针锁定(适用于游戏) |
allow-presentation | 允许使用 Presentation API (如投屏) |
allow-top-navigation | 允许 iframe 导航父页面(如 window.top.location = "..." ) |
安全最佳实践:
- 最小权限原则:只允许必要的功能
- 避免
allow-same-origin
滥用:危险!iframe 可以操控父页面
iframe 安全性考虑
1. 点击劫持
攻击者可以利用 iframe 将一个网页嵌入到透明的 iframe 中,诱导用户点击,从而劫持用户操作。这种情况下,用户以为在操作当前页面,实际上在点击 iframe 内的内容。
解决方法:在被嵌入的页面中设置 X-Frame-Options
响应头(需服务器配置),阻止页面被嵌入:
# 完全禁止被嵌入
Content-Security-Policy: frame-ancestors 'none';
# 仅允许同源嵌入
Content-Security-Policy: frame-ancestors 'self';
# 允许指定域名嵌入
Content-Security-Policy: frame-ancestors https://trusted-site.com;
优势:支持多域名白名单;现代浏览器优先支持 CSP 而非 X-Frame-Options
2. XSS
风险场景:
<!-- 动态src导致的DOM型XSS -->
<iframe src=javascript:alert(1)></iframe>
防护 csp 属性:
<iframe csp="script-src 'self'"></iframe>
优先通过后端设置 CSP:更安全、可靠、易维护
3. 跨域通信问题
不同域名下的页面不能直接通过 JavaScript 进行交互。这是为了防止跨站脚本攻击(XSS)。不过可以通过 postMessage
实现安全的跨域通信(见下文),但要注意跨域数据泄露。
iframe 跨域通信
由于浏览器的安全性限制,两个不同源的页面不能直接访问彼此的 DOM、不能通过常规的 JavaScript 互相操控。但可以使用 window.postMessage()
方法,在不同源的 iframe 和父页面之间安全地传递消息。
1. postMessage
父页面向 iframe 发送消息:
在父页面中,可以使用 iframe.contentWindow.postMessage
来向 iframe 发送消息
<iframe id="myFrame" src="<https://www.another-site.com>"></iframe>
<script>
const iframe = document.getElementById('myFrame');
iframe.contentWindow.postMessage('Hello from parent', '*');
</script>
postMessage
函数接受 2 个参数:第一个是传递的数据,第二个是传递给哪些源(域名)
这里的 '*'
表示消息可以发送到任何来源的 iframe。为防止跨域数据泄露起见,最好指定一个确切的目标源。
2. onMessage
iframe 接收消息并回应:
在 iframe 页面中,使用 window.addEventListener('message', callback)
来监听消息:
<script>
window.addEventListener('message', function(event) {
console.log('event object:', event);
// 确认消息来源的安全性
if (event.origin === 'https://www.example.com') {
// 执行一些操作
console.log(event.data)
// 回复消息
event.source.postMessage('Hello from iframe', event.origin);
}
});
</script>
3. 完整的安全通信流程
父页面 → 子 iframe
/* 父页面代码 */
const iframe = document.querySelector('iframe');
// 发送时限制目标origin
iframe.contentWindow.postMessage(
{ type: 'UPDATE', payload: { color: 'red' } },
'https://child-domain.com' // 确保只发送到指定域名
);
/* 子iframe代码 */
window.addEventListener('message', (event) => {
// 验证消息来源
if (event.origin !== 'https://parent-domain.com') return;
// 安全处理消息
if (event.data.type === 'UPDATE') {
applyStyles(event.data.payload);
}
});
子 iframe → 父页面
/* 子iframe代码 */
window.parent.postMessage(
{ type: 'READY' },
'https://parent-domain.com' // 确保只发送到父页面
);
/* 父页面代码 */
window.addEventListener('message', (event) => {
// 验证消息来源(需允许多个子域名时)
const allowedOrigins = [
'https://child-domain.com',
'https://app.child-domain.com'
];
if (!allowedOrigins.includes(event.origin)) return;
if (event.data.type === 'READY') {
initApp();
}
});
为什么需要双重验证?
验证环节 | 作用 | 未验证的风险 |
---|---|---|
发送方的 targetOrigin | 确保消息不会发错目标 | 消息可能泄露给恶意网站 |
接收方的 event.origin | 确认消息的真实来源 | 攻击者可以伪造消息 |
4. 安全增强技巧
消息签名(防篡改)
// 发送方签名
const payload = { data: '敏感操作' };
const signature = crypto.sign('SHA256', payload, privateKey);
parent.postMessage(
{ payload, signature },
'https://parent-domain.com'
);
// 接收方验证签名
if (!crypto.verify(event.data.payload, event.data.signature, publicKey)) {
throw new Error('消息被篡改!');
}
防重放攻击
// 发送方添加时间戳和 nonce
const message = {
data: '支付请求',
timestamp: Date.now(),
nonce: crypto.randomUUID()
};
// 接收方检查时效性
if (Date.now() - event.data.timestamp > 5000) {
throw new Error('消息已过期');
}