浏览器的同源策略

本文最后更新于:8 个月前

介绍浏览器的同源策略、以及如何解决同源策略带来的跨域问题

浏览器的同源策略

一、同源策略

所有浏览器都遵循的一个策略。A 网页设置的 Cookie,B 网页不能打开,除非这两个网页“同源”

定义

  • 相同的协议(protocol),http与https不同
  • 相同的域名www.baidu.com 与 baidu.com不同
  • 相同的端口号(有些浏览器不要求,IE<9),80与8080不同

限制范围

  • 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB

    用户登录某个站点,站点后端服务器验证账号密码正确之后,会返回Cookie、Token 或者是用户名和密码给客户端浏览器,浏览器会将该信息保存到上述某一个当中,如果没有同源策略,恶意网站就可以通过脚本获得用户的数据

  • 无法接触非同源网页的 DOM

    来自一个源的JS只能读写自己源的DOM树。如果可跨源读写DOM的话,在页面里面使用iframe嵌入一个银行页面,就可以随意篡改银行页面的内容了。

  • 无法向非同源地址发送 AJAX 请求(可以发送,但浏览器会拒绝接受响应)

    一般而言,JS只能向同源的接口发送请求。如果向其它源发送请求时,一方面,浏览器发现JS向其它源的接口发送请求时会自动带上Origin头来标识来源,让服务器能够根据Origin判断要不要响应;另一方面,浏览器在接收到响应后,如果没有发现Access-Control-Allow-Origin允许发送请求的域进行请求,浏览器就不允许解析

另外,通过 JavaScript 脚本可以拿到其他窗口的window对象,比如 iframe节点的window对象

二、跨域

指由于浏览器同源策略,浏览器对其它域执行一些操作时会受到限制。下面将介绍如何去规避这些限制

在浏览器中,<script><img><iframe><link>这几个标签可以跨域加载,而不受浏览器的同源策略控制,这些带src属性的标签实际上是通过浏览器发送GET请求来加载。不同于普通请求(XMLHttpRequest)的是,通过src加载的资源,浏览器限制了JS对它们的读写权限,使其不能读写通过src加载返回的内容

除了上述几个标签,其它跨越请求,请求都会发送到跨域的服务器,并且服务器会返回数据,只不过浏览器“拒收”返回的数据

1、规避cookie的限制

一般cookie的限制出现在一级域名和次级域名之间

限制描述

Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。如果不做设置,次级域名无法与一级域名共享cookie

规避办法

通过设置document.domain共享 Cookie。

举例来说,A 网页的网址是http://w1.example.com/a.html,B 网页的网址是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享 Cookie。因为浏览器通过document.domain属性来检查是否同源。

1
2
// 两个网页都需要设置,A 和 B 两个网页都需要设置document.domain属性,才能达到同源的目的,因为设置document.domain的同时,会把端口重置为null,因此如果只设置一个网页的document.domain,会导致两个网址的端口不同,还是达不到同源的目的。
document.domain = 'example.com';

这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexedDB 无法通过这种方法,规避同源政策

另外,服务器也可以在设置 Cookie 的时候,指定 Cookie 的所属域名为一级域名,比如.example.com

1
Set-Cookie: key=value; domain=.example.com; path=/

这样的话,二级域名和三级域名不用做任何设置,都可以读取这个 Cookie。

2、iframe和多窗口

一定层度上可以当做是规避DOM限制,但又不局限于DOM限制

限制描述

iframe元素可以在当前网页之中,嵌入其他网页。每个iframe元素形成自己的窗口,即有自己的window对象。iframe窗口之中的脚本,可以获得父窗口和子窗口。但是,只有在同源的情况下,父窗口和子窗口才能通信;如果跨域,就无法拿到对方的 DOM。

比如,父窗口运行下面的命令,如果iframe窗口不是同源,就会报错。

1
2
3
4
5
document
.getElementById("myIFrame")
.contentWindow
.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.

规避办法

  • document.domain,如果两个窗口只是二级域名不同,可以通过上面提到的办法规避
  • 片段识别符,通过设置锚点,在URL的锚点里放置要传递的信息,通过修改iframe的src的方法(不会触发刷新),传递信息
  • 跨文档通信API —— postMessage()

postMessage()

HTML5 引入了一个API 跨文档通信 API(Cross-document messaging),这个 API 为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。

  • 参数一:具体的信息内容
  • 参数二:接受信息的窗口的源。如果是*,向所有窗口发送
1
2
3
4
// 父窗口打开一个子窗口
var popup = window.open('http://bbb.com', 'title');
// 父窗口向子窗口发消息
popup.postMessage('Hello World!', 'http://bbb.com');

父窗口和子窗口都可以通过message事件,监听对方的消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 父窗口和子窗口都可以用下面的代码,
// 监听 message 消息
window.addEventListener('message', function (e) {
console.log(e.data);
},false);
//注意:这里没有过滤信息的来源,任意网址发来的信息都会被处理,这是不安全、不推荐的,改进版如下:

window.addEventListener('message', function (e) {
if (e.origin !== 'http://aaa.com') return;
if (e.data === 'Hello World') {
e.source.postMessage('Hello', event.origin);
} else {
console.log(e.data);
}
},false);

message事件的参数是事件对象event,提供以下三个属性:

  • event.source:发送消息的窗口(引用值),这使得读写其它窗口的LocalStorage也成为可能
  • event.origin: 消息发向的网址
  • event.data: 消息内容

3、规避AJAX的限制

同源政策规定,AJAX 请求只能发给同源的网址,否则就报错

规避办法:

  • 代理服务器,浏览器请求同源服务器,再由同源服务器请求外部服务(服务器没有同源策略一说)
  • JSONP,借助<script>标签不受同源策略限制的特点,通过 脚本代码传递数据
  • WebSocket,通信协议,不实行同源策略
  • CORS

JSONP

最大特点就是简单易用,没有兼容性问题,老式浏览器全部支持,服务端改造非常小

做法:

  1. 网页添加一个<script>元素,向服务器请求一个脚本,这不受同源政策限制,可以跨域请求

    1
    2
    <script src="http://api.foo.com?callback=bar"></script>
    /*注意,请求的脚本网址有一个callback参数(?callback=bar),用来告诉服务器,客户端的回调函数名称*/
  2. 服务器收到请求后,拼接一个字符串,将 JSON 数据放在函数名里面,作为字符串返回(bar({...})

  3. 客户端会将服务器返回的字符串,作为代码解析(因为浏览器认为,这是<script>标签请求的脚本内容)。这时,客户端只要定义了bar()函数,就能在该函数体内,拿到服务器返回的 JSON 数据

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
//客户端
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};
1
2
3
4
//服务端返回以下内容
foo({
'ip': '8.8.8.8'
});

由于<script>元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo函数,该函数就会立即调用。作为参数的 JSON 数据被视为 JavaScript 对象,而不是字符串,因此避免了使用JSON.parse的步骤。

WebSocket

WebSocket 是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。是server push技术的一种很好的实现

下面是一个例子,浏览器发出的 WebSocket 请求的头信息

1
2
3
4
5
6
7
8
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

上面代码中,有一个字段是Origin,表示该请求的请求源(origin),即发自哪个域名。

正是因为有了Origin这个字段,所以 WebSocket 才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就会做出如下回应。

1
2
3
4
5
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

CORS

CORS 是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,属于跨源 AJAX 请求的根本解决方法。相比 JSONP 只能发GET请求,CORS 允许任何类型的请求

参考文章:浏览器模型 - 同源限制 - 《阮一峰 JavaScript 教程》 - 书栈网 · BookStack


浏览器的同源策略
http://timegogo.top/2023/03/22/前端工程化/安全:浏览器的同源策略/
作者
丘智聪
发布于
2023年3月22日
更新于
2023年7月16日
许可协议