嵌入内容的 Storage Access API
了解嵌入文档如何检查并请求未分区 Cookie 的访问权限,用户确认与浏览器支持意味着什么,以及如何在访问被拒绝时设计回退路径。
Storage Access API 允许嵌入文档向浏览器请求访问自己的未分区 Cookie,在某些浏览器中还包括其他存储,而这种访问在该上下文中原本被阻止或被分区。嵌入文档用 document.hasStorageAccess() 检查当前状态,用 document.requestStorageAccess() 发出请求。结果由浏览器而不是页面决定,通常需要用户手势,有时还会弹出提示。授予的访问只是针对某个顶层站点下某一个嵌入文档的狭窄例外,页面需要为被拒绝和不支持的结果预先定义路径。
Storage Access API 决定什么
许多浏览器会隔离或阻止第三方嵌入文档能够访问的状态。嵌入式登录组件、支付表单、评论服务或客服聊天可能会发现:用户访问其自有站点时设置的 Cookie,在另一个站点的页面内部不会被发送。Storage Access API 是这类文档询问该状态能否重新可用的标准方式。MDN 的 Storage Access API 概述和 PrivacyCG 规范草案描述了这一模型。
需要抓住的词是请求。嵌入文档只能提出请求,不能进行配置。浏览器按自己的规则判断请求是否被允许、是否必须询问用户,以及用户此前与该嵌入站点有过哪些交互。这些规则因浏览器和版本而异,也可能在不同发布之间变化。页面可以说明自己请求什么,但不能承诺用户会看到什么。
用户确认正是这个 API 采用这种形态的原因。浏览某个站点的人,不一定愿意让该站点共享其中嵌入的第三方服务所保存的状态。因此浏览器会询问,方式可能是弹出提示,其中写明嵌入站点以及它所在的页面,也可能依据自己关于此前交互的规则。授予记录的是用户的选择,而不是页面的偏好。
范围与决定本身同样重要。被授予的请求只适用于该嵌入文档,以及它所嵌入的顶层上下文。它不会改变其他框架能读取什么,不会延续到另一个顶层站点,也不能说明其他存储 API,更不能说明这个人是谁。Cookie 是该 API 的核心对象;是否包含其他存储类型取决于浏览器和版本,所以请查阅你所测试浏览器的文档。
分区键,也就是浏览器按顶层站点隔离状态的机制,在浏览器存储分区与隐私中有说明。Storage Access API 是建立在这条边界之上的例外,并不取代它。至于哪种存储机制适合哪类数据这个更大的问题,请参阅 Cookie、localStorage 和 IndexedDB:状态应放在哪里。
这个 API 也不是用来了解某个人或跨站点关联会话的手段。它的存在,是为了让合法的嵌入服务,例如用户主动与之交互的登录或支付组件,在浏览器限制第三方状态时仍能工作。如果文档在没有对用户明确说明理由的情况下请求访问,用户就没有做出决定的依据,这也无益于使用页面的人。
检查状态与请求访问
document.hasStorageAccess() 返回一个 Promise,对当前文档解析为布尔值。它读取当前状态,不会请求任何东西,因此可以在加载时运行,不需要用户手势。结果为 true 表示文档目前可以访问它的未分区 Cookie。原因可能是此前已授予访问、文档并未被嵌入,或者在此配置下浏览器没有限制第三方 Cookie,所以仅凭 true 并不能证明发生过授予。
document.requestStorageAccess() 才是请求本身。它同样返回 Promise,授予访问时解析,未授予时拒绝。它需要短暂的用户激活,例如在嵌入文档内部的点击或按键,所以从定时器或加载时调用,预期会被拒绝,除非之前已经授予过权限,此时部分浏览器无需手势即可解析。用户拒绝提示、浏览器规则表明请求不被允许,或者之前的拒绝被记住时,调用同样可能被拒绝。请把拒绝当作正常结果处理,而不是需要掩盖的异常。requestStorageAccess() 参考页列出了相关条件。
对大多数嵌入式组件,一个简短的流程就够用。加载时先检测这些方法是否存在,并调用 hasStorageAccess()。如果结果为 true,就继续走正常的已登录路径。如果为 false,就显示一个控件,说明组件需要什么,并且只在这个控件的点击处理函数中调用 requestStorageAccess()。Promise 解析之后,重新加载或重新请求依赖这些 Cookie 的状态,再用 hasStorageAccess() 确认一次,然后才显示已登录内容。
async function showAccountState(button) {
if (!('hasStorageAccess' in document)) return renderSignedOut('unsupported');
if (await document.hasStorageAccess()) return renderSignedIn();
button.hidden = false;
button.addEventListener('click', async () => {
try {
await document.requestStorageAccess();
return (await document.hasStorageAccess()) ? renderSignedIn() : renderSignedOut('denied');
} catch {
return renderSignedOut('denied');
}
});
}
之后再次访问时,请重新调用 hasStorageAccess(),不要假设以前的授予仍然有效。浏览器按各自的节奏记住或让决定过期,用户可以重置站点权限,配置文件也可能被清除。每次加载都检查状态的文档,从浏览器当前的回答出发,不会把过时的假设从一次访问带到下一次。Storage Access API 使用指南以更多细节展示了同样的模式。
页面层面还有几个条件。嵌入文档需要处于安全上下文。如果框架被沙箱化,嵌入它的页面必须允许存储访问沙箱令牌(allow-storage-access-by-user-activation),同时允许脚本所需的令牌,例如 allow-scripts 和 allow-same-origin。Permissions Policy 也可以限制某个框架使用 storage-access 功能。请求被立即拒绝时,先检查框架属性和嵌入页面的策略,再去假设是用户做了决定。
在浏览器支持的情况下,Permissions API 会报告 storage-access 权限,取值为已授予或待询问;规范不会暴露已拒绝状态,所以被拒绝的请求读起来仍是待询问。它可以在请求之前读取状态,但不能用来改变状态。各浏览器对这种查询的支持不同,所以把它当作可选信号,并让 hasStorageAccess() 继续作为你据以行动的状态。
授予并不会改写 Cookie 规则。要在跨站上下文中发送的 Cookie,在对请求应用 SameSite 规则的浏览器里仍需要 SameSite=None 和 Secure,审查时应记录实际观察到的属性。如果 hasStorageAccess() 返回 true 之后组件看上去仍未登录,先检查 Cookie 属性和请求的来源,再去怀疑 API。
浏览器差异与支持情况
浏览器行为是这个主题中变化最大的部分。基于 Chromium 的浏览器、Firefox 和 Safari 都提供该 API,但它们在提示何时出现、此前是否需要把嵌入站点作为顶层页面交互过、决定会被记住多久,以及授予包含哪些存储等方面各不相同。有些浏览器还会应用自己的启发式规则或站点关系,在没有可见提示的情况下授予或拒绝访问。这些都是浏览器的决定,页面脚本无法强制。
请把支持情况当作需要按浏览器和版本测量的对象。像 'requestStorageAccess' in document 这样的功能检测,只能告诉你方法存在,不能告诉你调用会弹出提示、静默解析还是被拒绝。请查阅 MDN 的兼容性表和厂商指南,例如 Chrome 的 Storage Access API 指南,并记录你测试的版本,因为同一个页面在浏览器更新之后可能表现不同。
各浏览器对第三方 Cookie 的默认立场也不同。有的默认阻止或分区,有的把选择交给用户,企业策略还可能再次改变结果。在不限制第三方 Cookie 的浏览器中,hasStorageAccess() 可能在没有任何请求的情况下返回 true。在限制它们的浏览器中,同一个页面就需要发出请求。一种配置下的测试结果不能说明另一种配置,这正是审查记录要写明浏览器及其设置的原因。
嵌入站点和顶层站点都是决定的一部分。用户可能看到同时写明两者的提示,而针对某一组站点的授予,不能说明另一组站点的情况。如果同一个组件出现在你运营的多个站点上,就要为每一组站点分别测试和说明,并预期用户可能不止一次看到这个请求。
不要把一次成功的授予解读为第三方存储总体上未分区的证据。授予只是针对一个嵌入文档的例外,原因是用户对自己选择使用的服务做出了明确的操作。对于其他框架、其他站点和之后的访问,仍然以浏览器的分区规则为准。如果某个功能在测试中只有在被授予访问后才能工作,那么设计就依赖于一个例外,产品应当在自己的文档中说明这种依赖。
为被拒绝和不支持的结果做设计
有三种结果需要设计好路径:已授予、已拒绝和不支持。已授予时继续正常的已登录状态。已拒绝表示用户拒绝了、浏览器拒绝了调用,或者之前的拒绝被记住了。不支持表示方法缺失,或者浏览器对该功能的处理方式不同。后两种各自都需要一个可见且可用的状态,而不是空白框架或错误横幅。
最可靠的回退是第一方路径。嵌入组件显示未登录视图,并提供一个明确的操作,在服务自己的窗口或标签页中打开服务,浏览器会在那里应用第一方规则。登录后,用户回到嵌入它的页面,组件使用不依赖第三方 Cookie 的状态,例如通过消息传递的短期值,接收方会按预期来源检查这条消息。
让未登录状态保持有用。公开内容应当正常显示,用户已输入的草稿应当保留,界面应当用平实的语言说明缺少什么:服务需要获得许可,才能在这个页面内使用它保存的登录状态,用户也可以不授权而继续。不要反复询问。被拒绝之后,提供第一方操作,让用户自己选择何时重试,因为重复调用无论如何都可能在没有提示的情况下被拒绝。
要在点击之前写好说明,而不是等浏览器响应之后。控件旁边的文字应当说明嵌入服务将能使用什么,浏览器会用自己的措辞请求确认,以及用户拒绝后仍然可以做什么。保持简短,并避免把浏览器提示说成用户必须接受的东西。
不要围绕提示来构建方案。页面无法回答、隐藏或抑制浏览器提示,也不应当在真正的用户操作之外尝试触发提示。当存在第一方路径时,不要向用户描述这个提示是服务工作所必需的。提示的措辞和时机属于浏览器。页面的职责是在点击之前显示说明,说明嵌入服务将能读取什么以及原因。
请使用功能检测和失败处理,而不是检查用户代理。根据方法是否存在以及 Promise 的结果来分支,而不是根据浏览器名称或版本字符串,这样当浏览器更新增加或改变支持时,就不需要修改代码。为了自己的诊断,记录结果的类别,例如已授予、已拒绝、不支持或无手势被拒绝,并且不要在记录中包含 Cookie 值和账户标识符。
审查自己拥有的嵌入式组件
请用你自己拥有的组件测试这个 API,把它嵌入同样由你拥有的测试顶层站点,这样你可以控制两个来源和 Cookie。使用两个不同的可注册域名,或者两个浏览器视为不同站点的本地主机名,让框架确实是跨站的。在一个子域名上的测试页面,如果嵌入同一可注册域名下的另一个子域名,就属于同站,不会触发这项限制。
请记录每次运行的条件,而不是假设各浏览器之间表现一致:浏览器名称和版本、顶层站点、嵌入来源、被测 Cookie 的 SameSite 和 Secure 属性、iframe 是否被沙箱化或带有 allow 属性、调用之前是否有用户手势、hasStorageAccess() 的观察结果、调用的结果,以及在可用时的权限状态。记录不同的两次运行属于不同的实验。
每次运行都从已知的 Cookie 状态开始。Cookie 管理说明了如何为浏览器会话预加载 Cookie,浏览器多账户隔离说明了如何让上下文保持相互独立。这里需要干净的起始状态,因为上一次运行遗留的 Cookie 可能让被拒绝的路径看起来像已授予的路径。
BotBrowser 支持 --bot-cookies,它可以在启动时或按 BrowserContext 注入 Cookie,因此在审查自有嵌入流程时,每个测试上下文都可以从自己有文档记录的 Cookie 状态开始。BotBrowser 不会授予或拒绝 Storage Access API 请求,不会回答或抑制浏览器的权限提示,也不会改变浏览器允许哪些嵌入文档使用未分区存储;这些结果仍由浏览器和用户决定。
已授予的情形,请在被测浏览器中用真实的用户手势和真实的决定来运行。如果提示无法在自动化运行中应答,就手动执行该情形,并记录为手动结果。不要用注入的 Cookie 代替授予,因为那样测试的是已登录界面,对请求本身没有任何说明。
对每个结果都要做狭义解读。一次已授予的运行表明:这个版本的浏览器在这次手势之后,允许这个嵌入来源处于这个顶层站点之下。它并不表明其他浏览器的行为,不表明其他框架可以读取该 Cookie,也不表明第三方存储总体上是开放的。
在浏览器大版本发布、你的 Cookie 属性变化、嵌入页面的框架属性或策略变化,以及组件的登录流程变化之后,请重复这个矩阵。最近一次被接受的运行记录是基线,浏览器行为的差异是需要解读的发现,而不是靠重试掩盖的失败。
执行存储访问检查
在你支持的每个浏览器和版本中,对自己拥有的嵌入式组件执行这些检查,并为每一项记录通过或失败。
- 状态与请求。如果页面在加载时调用
hasStorageAccess()且不弹出提示,只在点击处理函数中调用requestStorageAccess(),并且记录分别显示了两个结果,则通过。如果请求在加载时运行,或两个结果被混在一起,则失败。 - 手势与拒绝。从没有任何先前授予的状态开始,先在没有用户手势时调用一次请求,再在有用户手势时调用一次。如果第一次调用被当作拒绝处理并显示可见的未登录状态,第二次如实报告其真实结果,则通过。
- 已授予路径。得到已授予的结果后,确认
hasStorageAccess()返回true,且组件使用预期的 Cookie 显示已登录状态。如果在hasStorageAccess()为false时出现已登录状态,则失败。 - 已拒绝路径。拒绝提示,或使用调用会被拒绝的配置。如果组件显示已定义的第一方或未登录回退,保留公开内容和已输入的草稿,并且不会自行再次提示,则通过。
- 不支持路径。在缺少这些方法的浏览器或配置中,如果组件通过功能检测到达同样已定义的回退,并且不根据浏览器名称分支,则通过。
- 环境记录。如果记录列出了浏览器和版本、顶层站点、嵌入来源、
SameSite和Secure属性、沙箱和allow设置,以及观察到的权限状态,则通过。如果报告结果时缺少这些信息,或假定结果适用于其他浏览器,则失败。 - 授予的范围。把同一个组件嵌入第二个测试顶层站点。如果该站点得到自己的结果,并且没有假定第一个站点的授予同样适用,则通过。