跨域 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-PolicyHTTP 标头来说,它总是*'、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 
