跨域 iframe 内剪切板 Clipboard_API 报错
跨域 iframe 内剪切板 Clipboard_API 报错
- 复现问题
- 解决问题
- 权限的查询、变更
- 控制 iframe 的权限
- 嵌入式内容的继承策略
- [allowlist 的特殊值](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Guides/Permissions_Policy#允许列表)
- 解决方案
- 内层的网页导航到其他的网站
- 如果内层的网页又嵌套其他的 `iframe` 怎么办???问题未解决
- 参考
复现问题
有两个网页:
- 内层网页
http://localhost:8081/index.html
实现了简单的复制粘贴功能。 - 外层网页
http://localhost:8080/index.html
也实现了相同的复制粘贴功能(内外层网页实现复制粘贴的代码是完全相同的)。同时又以iframe
的形式嵌套了内层网页。外层和内层的端口号分别为 8080 和 8081,所以是跨域的。
效果如下图:
控制台打印了初始的权限状态。
内层 index.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>内层</title>
</head>
<body>
<h1>内层</h1>
1 <input id="input1" type="text" value="内层"/>
<button onclick="copy()">copy</button>
<br/>
2 <input id="input2" type="text" value=""/>
<button onclick="paste()">paste</button></body>
<!-- 引入 js -->
<script type="text/javascript" src="index.js"></script>
</html>
外层 index.html
在上述代码的基础上添加了 iframe
标签:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>外层</title>
</head>
<body>
<h1>外层</h1>
1 <input id="input1" type="text" value="外层"/>
<button onclick="copy()">copy</button>
<br/>
2 <input id="input2" type="text" value=""/>
<button onclick="paste()">paste</button>
<br/><!-- iframe 8081 端口 -->
<iframe style="margin-top: 10px" width="500px" height="200px"src="http://localhost:8081/index.html"></iframe>
</body>
<!-- 引入 js -->
<script type="text/javascript" src="index.js"></script>
</html>
index.js
const title = document.title;/*** 查询权限** @param permissionName*/
function queryPermission(permissionName) {navigator.permissions.query({ name: permissionName }).then((permissionStatus) => {console.log(`${title} ${permissionName} 权限状态为 ${permissionStatus.state}`);permissionStatus.onchange = () => {console.log(`${title} ${permissionName} 权限状态已更改为 ${permissionStatus.state}`);};});
}// 查询权限 clipboard-read、clipboard-write
queryPermission("clipboard-read")
queryPermission("clipboard-write")/*** 将输入框 1 的文本复制到剪贴板*/
function copy() {navigator.clipboard.writeText(document.getElementById('input1').value).then(() => {alert('复制成功');});
}/*** 将剪贴板的文本复制到 2 输入框*/
function paste() {navigator.clipboard.readText().then((clipText) => {document.getElementById('input2').value = clipText;alert('粘贴成功');})
}
测试时,外层的 copy、paste 都没问题,内层的 copy、paste 都会报权限相关的错误:
解决问题
权限的查询、变更
查阅 Clipboard - Web API | MDN 发现要读写剪切板,需要分别获取 clipboard-read 、clipboard-write 两个权限。可以使用 Permissions:query() 方法 来查询权限,即 index.js
中的 queryPermission
方法。PermissionStatus:state 属性 - Web API | MDN 中指出权限有三种状态:
'granted'
用户或代表用户的用户代理已明确许可使用此技术特性。调用者可以使用该特性,而无需用户代理请求用户的许可。
'denied'
用户或代表用户的用户代理已拒绝访问此技术特性。调用者无法使用该特性。
'prompt'
用户未明确授予使用该特性的权限(即与拒绝相同)。这还意味着,如果调用者尝试使用该特性,用户代理将提示用户授予许可,否则对该特性的访问将被拒绝。
-
上图中外层
clipboard-write
的初始状态为granted
,所以可以直接调用clipboard.writeText()
方法,无需任何授权。 -
外层
clipboard-read
的初始状态为prompt
,在调用clipboard.readText()
方法时会弹出询问框,要求用户授权,如下图。
授权后,控制台打印出权限变更:
- 而内层的两个权限状态都是
denied
,所以调用Clipboard
的任何方法都会报错。
控制 iframe 的权限
<iframe>:内嵌框架元素 中提到 iframe 的 allow 属性可以控制 iframe 的全系:
allow
用于为
<iframe>
指定其权限策略。该策略根据请求的来源规定<iframe>
可以使用哪些特性(例如,访问麦克风、摄像头、电池、web 共享等)。示例请参见Permissions-Policy
中的 iframe ( iframe 这个链接需要看英文版,中文版的翻译缺少一些内容)。备注: 通过
allow
属性指定的权限策略会在Permissions-Policy
标头指定的策略基础上进一步地限制。它不会替换原有策略。
权限策略 - iframe 语法 - HTTP | MDN 有相似的内容。
对于一个
<iframe>
来说,其允许的源也必须在父页面的允许列表中。由于这种继承行为,最好在 HTTP 头中指定最广泛的可接受的特性支持,然后在每个<iframe>
中指定你需要的支持子集。一般的语法看起来像这样:
<iframe src="<origin>" allow="<directive> <allowlist>"></iframe>
要允许所有源访问地理位置信息的访问,你可以这样做:
<iframe src="https://example.com" allow="geolocation *"></iframe>
例如,要阻止对地理位置信息的访问,你可以这样做:
<iframe src="https://example.com" allow="geolocation 'none'"></iframe>
要将一种策略运用到当前的源和其他的源中,你可以这样做:
<iframesrc="https://example.com"allow="geolocation 'self' https://a.example.com https://b.example.com"></iframe>
这很重要:默认情况下,如果一个
<iframe>
导航到另一个源,策略就不会应用到<iframe>
导航到的源。通过在allow
属性中列出<iframe>
导航到的源,应用于原始<iframe>
的许可策略将被应用于<iframe>
导航到的源。通过在
allow
属性中包含一个分号分隔的策略指令列表,可以同时控制多个特性。<iframesrc="https://example.com"allow="geolocation 'self' https://a.example.com https://b.example.com; fullscreen 'none'"></iframe>
值得特别一提的是
src
值。我们在上面提到,使用这个 allowlist 值意味着相关的特性在这个<iframe>
中是被允许的,只要加载到其中的文档与它的 src 属性中的 URL 来自同一来源。这个值是allow
中所列特性的默认allowlist
值,所以下面这些是等价的:<iframe src="https://example.com" allow="geolocation 'src'"><iframe src="https://example.com" allow="geolocation"></iframe ></iframe>
备注: 你会注意到,
<iframe>
策略的语法与Permissions-Policy
标头的语法有些不同。前者仍然使用与旧的特性策略规范相同的语法,它被权限策略所取代。嵌入式内容的继承策略
脚本会继承其浏览上下文的策略,而不管其源如何。这意味着顶层的脚本会继承主文件的策略。
所有
<iframe>
都继承其父页的策略。如果<iframe>
有一个allow
属性,并且父页面有一个Permissions-Policy
标头,父页面和allow
属性的策略将被合并,使用最严格的子集。对于一个<iframe>
来说,要启用一种特性,其源必须是在父页和allow
属性的允许列表中。在一个策略中禁用一个特性是单向的切换。如果子框架被它的父框架禁用了一个特性,那么这个子框架就不能重新启用它,这个子框架的任何后代也不能。
allowlist 的特殊值
一个允许列表是一系列源的列表,它采取一个或多个包含在括号中的下列值,用空格隔开:
*
:该特性将被允许在本文档和所有嵌套浏览上下文(<iframe>
)中使用,无论其源如何。()
(空允许列表):该特性在顶层和嵌套浏览环境中被禁用。等价的<iframe>
的allow
属性值是'none'
。self
:该特性只允许在本文档和同一来源的所有嵌套浏览环境(<iframe>
)中使用。在嵌套浏览的情况下,该特性不允许出现在跨源文件中。self
可以被认为是https://your-site.example.com
的简写。对应的<iframe>
的allow
属性值是self
。src
:只要载入该框架的文件与该框架 src 属性中的 URL 来源相同,该特性在该<iframe>
中就被允许。这个值只用于<iframe>
的allow
属性,并且是<iframe>
的默认allowlist
值。"<origin>"
:该特性允许用于特定的源(如"https://a.example.com"
)。源应该用空格隔开。请注意,<iframe>
allow 属性中的源是不加引号的。备注: 指令有一个默认的允许列表,对于
Permissions-Policy
HTTP 标头来说,它总是*
'、self
或none
之一,如果它们没有明确地列在策略中,则管理默认行为。这些都是在各个指令参考页中指定的。对于<iframe>
的allow
属性,默认行为总是src
。
解决方案
有了上面的理论基础,问题便很好解决,只需将 iframe
的 allow
属性设为 clipboard-read 'src'; clipboard-write 'src'
即可,或者简写为 clipboard-read; clipboard-write
,因为 iframe
默认的 就是 src
。
<!-- iframe 8081 端口 -->
<iframe style="margin-top: 10px" width="500px" height="200px"allow="clipboard-read 'src'; clipboard-write 'src'"src="http://localhost:8081/index.html"></iframe>
<!-- allow="clipboard-read; clipboard-write"-->
</body>
内层功能正常:
内层的网页导航到其他的网站
修改后的内层 index.html
添加了一个跳转内层 2 的 <a>
标签
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>内层</title>
</head>
<body>
<h1>内层</h1>
1 <input id="input1" type="text" value="内层"/>
<button onclick="copy()">copy</button>
<br/>
2 <input id="input2" type="text" value=""/>
<button onclick="paste()">paste</button><br/>
<!-- 跳转内层2 -->
<a href="http://localhost:8082/index.html">内层2</a>
<br/>
</body>
<!-- 引入 js -->
<script type="text/javascript" src="index.js"></script>
</html>
初始状态:
点击 “内层 2” 链接跳转后:
从上图可以看出,点击链接跳转后的内层 2 的权限都是 denied
,无法调用剪切板。解决办法有多个:
- 如果明确知道
iframe
中可能导航到的网站有哪些,或者只想让iframe
中的某些网站有剪切板权限,可以将allow
设为clipboard-read http://localhost:8081 http://localhost:8082; clipboard-write http://localhost:8081 http://localhost:8082
。 - 如果想让
iframe
中的所有网站都有剪切板权限,可以将allow
设为clipboard-read *; clipboard-write *
修改后的外层 index.html iframe
<!-- iframe 8081 端口 -->
<iframe style="margin-top: 10px" width="500px" height="200px"allow="clipboard-read http://localhost:8081 http://localhost:8082; clipboard-write http://localhost:8081 http://localhost:8082"src="http://localhost:8081/index.html"></iframe>
<!-- allow="clipboard-read *; clipboard-write *"-->
测试结果:
如果内层的网页又嵌套其他的 iframe
怎么办???问题未解决
修改后的内层 index.html
添加了一个嵌套内层 2 的 <iframe>
标签
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>内层</title>
</head>
<body>
<h1>内层</h1>
1 <input id="input1" type="text" value="内层"/>
<button onclick="copy()">copy</button>
<br/>
2 <input id="input2" type="text" value=""/>
<button onclick="paste()">paste</button><br/>
<!-- 跳转内层2 -->
<a href="http://localhost:8082/index.html">内层2</a>
<br/>
<!-- iframe 8082 端口 -->
<iframe style="margin-top: 10px" width="500px" height="200px"src="http://localhost:8082/index.html"></iframe>
</body>
<!-- 引入 js -->
<script type="text/javascript" src="index.js"></script>
</html>
修改后的外层 index.html iframe
<!-- iframe 8081 端口 -->
<iframe style="margin-top: 10px" width="550px" height="400px"allow="clipboard-read *; clipboard-write *"src="http://localhost:8081/index.html"></iframe>
初始状态:
外层 iframe
的 allow
设为 clipboard-read *; clipboard-write *
也不能解决问题,难道只能逐层去设置的 iframe
的 allow
???
参考
-
Clipboard - Web API | MDN
-
Clipboard.writeText() - Web API | MDN
-
Clipboard.readText() - Web API | MDN
-
权限 API - Web API | MDN
-
使用权限 API - Web API | MDN
-
Permissions:query() 方法 - Web API | MDN
-
PermissionStatus - Web API | MDN
-
PermissionStatus:state 属性 - Web API | MDN
-
<iframe>:内嵌框架元素
-
Permissions-Policy - HTTP | MDN
-
权限策略 - iframe 语法 - HTTP | MDN
-
权限策略 - allowlist- HTTP | MDN