- 作者:老汪软件技巧
- 发表时间:2024-11-21 10:03
- 浏览量:
❌
❌
站点(site)
说到同源,大家可能会想到 cookie 中的 sameSite 属性,可以让相同的站点共享 cookie。
那么大家有没有想过,怎么样才算作 same site,也就是同站呢?
顶级域名、二级域名、三级域名
先给大家科普一下什么是域名中的顶级域名、二级域名和三级域名。
顶级域名(Top Level Domain, TLD)
顶级域名是域名的最高层级,位于最右侧的一部分。常见的顶级域名有 .com、.org、.net、以及国家/地区代码顶级域名如 .cn(中国)、.jp(日本)等。顶级域名由互联网域名分配机构(如 ICANN)管理。
例如在 中,.com 就是顶级域名。
二级域名(Second Level Domain, SLD)
二级域名是顶级域名的左边一部分,一般由用户注册的名称构成。二级域名通常是网站的主要标识,且直接位于顶级域名之下。
例如在 中,baidu 就是二级域名。大家注意,二级域名是不带上后面的顶级域名的。
三级域名(Third Level Domain,也叫作 Subdomain)
三级域名位于二级域名左侧,是二级域名的一个子域。网站可以在二级域名的基础上创建多个三级域名,以区分不同的服务或子站点。它可以进一步划分不同的网站部分或应用服务,例如 www、mail 等。
在 中,www 是三级域名;在 中,map 也是一个三级域名。大家注意,三级域名是不带上后面的二级域名和顶级域名的。
大家可以看看下图,来帮助理解和记忆。
这里提一下,因为翻译原因,有的人会将 Second Level Domain 翻译为一级域名,从而将 Third Level Domain 翻译为二级域名,其实它们都代表同一个东西,只是翻译不一样导致结果不一样,大家可以在面试的时候提一下,并且用英文名称可以避免歧义。
介绍
一个站点的域名,必须是要符合规则的,必须要含有公共后缀(Public Suffix List,这里列举了所有的公共后缀),例如 ,而不能是随意的例如 a.b.c.helloworld。
于是就有了一个名词:可登记域名(registrable domain) ,表示可以登记的有效域名,只有符合规则的域名才可以登记以供大家从互联网访问。
获取可登记域名的值的步骤为:
如果主机的公共后缀是 null,或者主机就等于公共后缀,则返回 null返回域名中公共后缀前面的单个部分字符串 + 公共后缀(也就是二级域名 + 顶级域名) ,如果主机末尾带有一个 .,则可登记域名结尾也会拼接上一个 .。
看起来有点难以理解,大家看看下面的例子应该就明白了:
主机公共后缀可登记域名
com
com
null
com
com
sub.
com
EXAMPLE.COM
com
.
com.
.
github.io
github.io
null
whatwg.github.io
github.io
whatwg.github.io
[2001:0db8:85a3:0000:0000:8a2e:0370:7334]
null
null
前面提到的同站的判断依据,就是比较两个站点的 site 值是否相同。获取 site 的值的步骤为:
如果站点的 origin 是一个不透明源,则返回这个 origin;如果 origin 的可登记域名是 null,则返回 (origin 的协议, origin 的主机);返回 ( origin 的协议, origin 的可登记域名) 。
如果两个站点的 site 值是相同的,则满足 sameSite。例如 的 site 值根据上面步骤得到的结果为 (https, ), 的结果为 (https, ),所以满足 sameSite 条件。
两个站点的 site 值是相同的,也就是同站(same site) ;值不同,也就是跨站(cross site) 。根据前面的公式可以得到一个结论,跨站一定跨域,跨域不一定跨站。
测试
咱们一起来做个测试,给满足 sameSite 条件的其中一个站点设置 cookie,并且设置 SameSite=strict(最严格的模式,只有满足同站才会自动携带 cookie),看看访问另外一个站点会不会自动携带 cookie。
通过 Express 框架运行两个页面,一个页面地址是 :3000,另一个是 :4000。根据上面获取 site 的步骤,我们可以知道这两个地址是同站的。
:3000 页面:
const express = require('express');
const fs = require('fs');
const app = express();
const buffer = fs.readFileSync('./index.html');
const html = new String(buffer).replace(/\n/g, '');
app.use('/', (_, res) => {
res.set('content-type', 'text/html');
// 设置 Set-Cookie 响应头
res.set('set-cookie', 'name=cookie_from_port_3000; SameSite=strict');
res.send(html);
});
app.listen(3000, () => {
console.log('页面启动在 localhost:3000');
});
:4000 页面:
const express = require('express');
const fs = require('fs');
const app = express();
const buffer = fs.readFileSync('./index2.html');
const html = new String(buffer).replace(/\n/g, '');
app.use('/', (_, res) => {
res.set('content-type', 'text/html');
res.send(html);
});
app.listen(4000, () => {
console.log('页面启动在 localhost:4000');
});
可以看到上面代码,我只给 3000 页面设置了 cookie,4000 页面并没有设置。我们来看一下结果,首先可以看到 3000 页面已经设置了 cookie,并且请求头没有 cookie。
刷新一下 3000 页面,可以看到此时请求头携带了 cookie:
我们再打开 4000 页面看一下:
可以看到,虽然 :3000 和 :4000 并不同源,但是他们满足同站,而 Set-Cookie 中的 SameSite 属性就是按照同站的规则来执行的。
通过 document.domain 绕开同源限制
通过 document.domain 属性可以获取当前页面用于安全检查的域。而通过设置 document.domain 属性,可以绕开同源策略的限制,从而允许同一主域下的不同子域页面相互访问对方的 DOM。
举个例子,假设有以下三个页面:
:443:一个位于子域的页面。:443:主域页面。:443:不同的域。
对于 :443 来说,默认情况下的元组源为:
在这种情况下,host 为 。因此, 仅信任自身,而不会自动信任 的其他子域。
但是如果我们设置了 document.domain,情况就不一样了。
假设在 和 的页面上都执行了以下代码:
document.domain = "example.com";
此时元组源变为:
现在,domain 字段设置为 。此时, 和 会被视为同源,因为它们共享了 domain 字段,允许在同一父域()下进行跨子域访问对方的 DOM。
只有当两个页面都将 document.domain 设置为相同的主域(如 )时,才允许跨子域的同步访问。如果一方未设置,访问仍会被阻止。
虽然规范没有明确说明,但是根据我的推测,在设置了 document.domain 后,同源策略使用的应该是同源域算法,而不再是同源算法。在设置了 document.domain 后,同源策略遵循同源域算法的规则,只要协议和 domain 值相同,就视为同源。大家可以看到这里忽略了端口号。
但是大家注意,document.domain 属性已经不推荐使用了,有些浏览器仍然支持该属性,但是不保证未来会不会被废弃。
设置失败场景
document.domain 在一些特定环境中无法使用,会设置失败:
避免使用 document.domain
document.domain 最初被用来允许跨子域的通信,可以绕开同源策略的限制。但是这种用法会削弱安全性,并带来难以管理的风险:
削弱同源策略:将 document.domain 设置为共享的基础域(例如 )后,多个子域(例如 和 )就可以互相访问彼此的 DOM。虽然这种方式在某些情况下很有用,但如果其中某个子域不安全,可能会带来安全隐患。共享主机的安全风险:在共享主机环境中,多个应用可能共享同一个域名或 IP 地址(但在不同端口上运行),而设置了 document.domain 以后会导致同源检查忽略端口号,从而引发信息泄露的风险。这样,如果一个子域或端口被入侵,就可能导致未经授权的访问。
由于这些安全问题,document.domain 正逐渐从 Web 平台中移除,这是一个较为漫长的过程,毕竟要考虑很多系统都是老代码,以及各种兼容性,但它的废弃已经在推进中了,以提高现代 Web 应用的安全性。
document.domain 的替代方案
想让具有相同父域的子域之间相互通信,我们可以使用其他方法来替代使用 document.domain:
postMessage() 方法:postMessage() 可以实现安全的跨源通信。不同源的窗口或 iframe 可以使用该方法发送消息,而接收方则需要验证消息来源的 origin,以确保数据的安全性。
// 在发送窗口中
otherWindow.postMessage("Hello", "https://another-subdomain.example.com");
// 在接收窗口中
window.addEventListener("message", (event) => {
// 验证来源
if (event.origin !== "https://expected-origin.com") return;
console.log("接收到的数据:", event.data);
});
MessageChannel 对象:MessageChannel 提供了一种更高级的通信方式,可以在两个窗口或 worker 之间创建专用的通信通道,实现更安全的双向通信,不依赖 document.domain。可以借助 postMessage,将 port2 传递给目标窗口,目标窗口用 port2 传递信息,主窗口用 port1 进行消息的监听。
// 在主窗口中
const channel = new MessageChannel();
iframe.contentWindow.postMessage("initiate", "*", [channel.port2]);
// 在 port1 上监听消息
channel.port1.onmessage = (event) => {
console.log("来自 iframe 的消息:", event.data);
};
跨域开放者策略(Cross-origin opener policies)
前面章节已经介绍过了一部分,这里就不再赘述重复内容了,大家可以点击链接过去再复习一下。
前面章节只介绍了跨域开放者策略 COOP 对 opener 属性的影响,这里再介绍一下对页面间通信的影响。
大家都知道两个网站之间可以使用 postMessage 或者 MessageChannel 来通信,无论两个网站是同源或非同源。就算是相同父域的子域,也不应该再通过修改 document.domain,而是使用 postMessage 或者 MessageChannel。
而对于通过 window.open 打开的跨域窗口,我们在设置了 COOP 后,会阻止源窗口与跨域窗口之间的通信,无法再使用 postMessage 和 MessageChannel。
对于 COOP 的三个值:
测试
我们来测试一下,不同的三个值会对源窗口与打开的跨域窗口之间的通信有何影响。
unsafe-none
依然是通过 Express 框架运行两个页面,一个页面地址是 :3000,另一个是 :4000,我们先来看看设置了 unsafe-none,其实也就相当于没有设置,两个页面可不可以通信:
:3000 页面:
const express = require('express');
const fs = require('fs');
const app = express();
const buffer = fs.readFileSync('./index.html');
const html = new String(buffer).replace(/\n/g, '');
app.use('/', (_, res) => {
res.set('content-type', 'text/html');
res.set('cross-origin-opener-policy', 'unsafe-none');
res.send(html);
});
app.listen(3000, () => {
console.log('页面启动在 localhost:3000');
});
<h1>3000 端口的前端创可贴h1>
<button id="btn">打开 4000 端口button>
<script>
btn.addEventListener('click', () => {
const popup = window.open('http://localhost:4000');
// 在 4000 页面 load 事件加载后调用,但是打开的是跨域页面,popup 所代表的 Window 对象只能访问受限的属性,无法监听 load 事件,通过 setTimeout 来模拟 4000 页面挂载完毕。
// 不使用 setTimeout,同步调用 postMessage 是不生效的,必须得等 4000 页面挂载完毕
setTimeout(() => {
popup.postMessage('你好我是 3000 端口', 'http://localhost:4000');
}, 1000);
});
script>
:4000 页面:
const express = require('express');
const fs = require('fs');
const app = express();
const buffer = fs.readFileSync('./index2.html');
const html = new String(buffer).replace(/\n/g, '');
app.use('/', (_, res) => {
res.set('content-type', 'text/html');
res.send(html);
});
app.listen(4000, () => {
console.log('页面启动在 localhost:4000');
});
<h1>4000 端口的前端创可贴h1>
<script>
window.addEventListener('message', e => {
if (e.origin !== 'http://localhost:3000') {
return;
}
console.log('接收到来自 3000 端口的消息', e);
});
script>
先来看看 3000 页面的响应头:
COOP 设置成功,我们再点击按钮打开 4000 端口页面看看:
可以看到,4000 页面可以接受到 3000 端口的消息,不受 COOP 为 unsafe-none 的影响。
same-origin
将 3000 端口 COOP 响应头设置为 same-origin:
res.set('cross-origin-opener-policy', 'same-origin');
打开 4000 页面:
可以看到,4000 页面没有接收到任何消息,所以 COOP 为 same-origin 时,打开的跨域窗口无法与源窗口通信。
same-origin-allow-popups
将 3000 端口 COOP 响应头设置为 same-origin-allow-popups:
res.set('cross-origin-opener-policy', 'same-origin-allow-popups');
打开 4000 页面:
可以看到,4000 页面可以接收到 3000 端口的消息,所以 COOP 为 same-origin-allow-popups 时,打开的跨域窗口可以与源窗口通信。
跨域嵌入策略(Cross-origin embedder policies)
嵌入策略 (Embedder Policy) 是浏览器用来控制跨域资源如何被嵌入和加载的安全机制。它通过指定策略值来决定哪些跨域资源可以被加载,以及在哪些情况下需要明确的服务器授权。以此来提升 Web 应用的安全性和隔离性。
通过 Cross-Origin-Embedder-Policy 响应头设置策略值,它的值可以是:
测试
咱们再来测试一下,仍然是通过 Express 框架,我们打开一个 :3000 页面,并且设置 Cross-Origin-Embedder-Policy 响应头。
unsafe-none
const express = require('express');
const fs = require('fs');
const app = express();
const buffer = fs.readFileSync('./index.html');
const html = new String(buffer).replace(/\n/g, '');
app.use('/', (_, res) => {
res.set('content-type', 'text/html');
res.set('cross-origin-embedder-policy', 'unsafe-none');
res.send(html);
});
app.listen(3000, () => {
console.log('页面启动在 localhost:3000');
});
<h1>前端创可贴h1>
<img
src="https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png"
alt=""
/>
我们请求了一张跨域的百度的 logo 图片,并且 Cross-Origin-Embedder-Policy 设置为 unsafe-none,我们来看一下页面:
可以看到,响应头正确设置,并且图片也正常返回,而且图片也正常携带了 cookie(我访问过百度页面,所以会携带 cookie)。所以,COEP 为 unsafe-none 时不会对跨域资源有任何影响。
require-corp
将 COEP 设置为 require-corp:
res.set('cross-origin-embedder-policy', 'require-corp');