chrome 在 115 版本引入了一个和 iframe 息息相关的新特性,叫做 unpartitioned third-party storage,虽然不是什么重大更新,但是这个还真有可能一不小心给我们的工作埋下暗坑,如果你项目里用到 iframe 集成其他项目的页面了,还正好涉及到了一些单点登录相关的功能,那为了避免喜提来自 chrome 的工作量,咱还是来了解一下比较好。
这是个什么东西?
首先我们要知道的是,在老版本的 chrome 中,localstorage 的访问只和所处页面是否同源有关,只要地址是同源的,比如 example.com。那么无论你访问的是下面哪种情况:
- 在 a.com 里通过 iframe 嵌入的 example.com
- 在 b.com 里通过 iframe 嵌入的 example.com
- 浏览器直接打开的 example.com
这三个 example.com 读写的 localstorage 实际上是通用的,我们可以写一个 demo 证明一下。
由于这个特性是默认开启的,所以如果你的版本大于 115 的话,就需要先访问 chrome://flags
把 Experimental third-party storage partitioning
设置为 Disabled。
假设我们有两个网站,b.html 长这样,它可以更新 localstorage 里一个名为 token 的字段。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网站B</title>
</head>
<body>
<h1>网站B</h1>
<button id="update-btn">刷新 token</button>
<button id="open-btn">新标签页打开网站B</button>
<p id="show">当前 localstorage 中 token 的值为:</p>
<input placeholder="设置 localstorage token" id="my-input" />
<button id="save-btn">保存</button>
</body>
<script>
const readLocalstorage = () => {
const value = localStorage.getItem('token')
document.getElementById('show').innerText = `当前 localstorage 中 token 的值为:${value}`
}
document.getElementById('save-btn').addEventListener('click', () => {
const value = document.getElementById('my-input').value
localStorage.setItem('token', value)
readLocalstorage();
})
document.getElementById('open-btn').addEventListener('click', () => {
window.open('http://localhost:9876', '__blank');
})
document.getElementById('update-btn').addEventListener('click', () => {
readLocalstorage();
})
readLocalstorage();
</script>
</html>
以及 a.thml,它只是单纯的用 iframe 集成了 b.html:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网站A</title>
</head>
<body>
<h1>网站A</h1>
<iframe src="http://localhost:9876" style="height: 100vh;width: 80vw;"></iframe>
</body>
</html>
现在我们用 serve 分别把 a.html 暴露在 127.0.0.1:9875,把 b.html 暴露在 127.0.0.1:9876。然后访问一下 a.html:

可以看到,不管有没有被 iframe 嵌套,127.0.0.1:9876 中的 b.html 始终可以访问到自己域下的最新 localstorage。
那如果把 Experimental third-party storage partitioning.
打开呢?

iframe 里边的 b.html 和直接标签页打开的 b.html 能访问到的 localstorage 被隔离开了,它们两个访问到的不再是同一个,而是各自独立的 localstorage。
开启了
Experimental third-party storage partitioning
,被 iframe 集成的网站的域会变成类似于a.com/b.com
,而直接打开的网站的域则是b.com
,两者互相独立。
会导致什么问题?
现在可以回想一下,你项目里有没有用 iframe 接入过其他项目的某个页面,并且当外部页面登录之后,会通过单点使得 iframe 里边的页面也完成登录。
如果有的话,那 iframe 里的页面如果执行了类似于 window.open 的操作,那大概率就会触发这个问题。
具体表现就是:本来我操作的页面(嵌在 iframe 里)是已经登录的,但是当打开一个新窗口后,突然弹回到登录页了。明明原来的页面登录状态还没有掉,新开的窗口页为什么一直提示未登录呢。
原因很简单,前端的登录状态一般都是通过把 token 保存在 localstorage 里实现的。而新开的窗口页无法访问到 iframe 里保存的 localstorage token 字段。就认为系统未登录了。
怎么解决?
有三种方案,第一种是临时过渡方案:参考 这里的方法,

根据你的情况从对应的链接里申请一个 token,然后放在你的网站上。之后这个检查就会被临时解除。但是要注意,最晚到 2024 年 9 月 4 号,这个方案就会被彻底关闭(这个实验特性被实装),所以你需要评估一下。
第二种方案要求你能修改 iframe 内嵌网站的代码,判断自己是否被 iframe 集成了,如果是的话就不在跳转,而是对外 postmessage,让 iframe 外边的网站再走一遍单点流程然后 window.open 就行了。
第三种方案就是通过 nginx 反代,把 iframe 里边集成的项目跟你外部的项目搞成同源的,这就彻底没这个问题了,但是改造成本可能不低。你可能得处理诸如字体、图片、接口请求、前端静态资源等一系列加载问题。
一些细节
当一个嵌在 iframe 里的页面触发了这个特性时,在 localstorage 里会将其标注为 is-third-party
:

并且,这个特性的触发标准是 top-level origin,而不是我们所熟知的同源策略 same-site origin。所以你可以发现哪怕端口不同(不同源),我们实际上也不会触发这个特性:

参考
- Local (web) storage affected by "Experimental third-party storage partitioning" · GoogleChromeLabs/privacy-sandbox-dev-support · Discussion #157 (github.com)
- Participate in deprecation trial for unpartitioned third-party storage, Service Workers, and Communication APIs - Chrome for Developers
- Storage Partitioning - Chrome for Developers --- 存储分区 - Chrome for Developers
- Origin Trials (chrome.com)
- HTML Standard (whatwg.org)