[关闭]
@yangfch3 2017-03-07T14:31:50.000000Z 字数 4030 阅读 3550

浏览器同源策略与跨域详解

FE


同源策略是浏览器采取的安全策略,目的是为了保护用户信息的安全。同源策略的根源是 浏览器对非同域页面的不信任

哪些是不受同源策略限制的:

  1. 提交表单不受同源政策的限制

  2. 同时一般情况下由 DOM 发起的网络请求(例如:img, script 等 DOM 元素加载资源)也是不受同源策略的限制的

    之所以说一般情况是因为还存在内容安全策略(Content Security Policy,CSP)这个东东,可以通过 HTTP 响应头的 CSP 字段告知浏览器我当前这个页面允许资源加载的白名单:
    image_1b6suufi61ovp13dd1crsbi27dt9.png-74.7kB

随着互联网的发展,"同源政策"越来越严格。目前,如果非同源,受同源策略限制的:

  1. Cookie、LocalStorage 和 IndexDB 无法读取。
  2. (不同源的窗口或框架)DOM 无法获得。

  3. 非同源页面向我们的源发起的 AJAX 请求无法正常接收获得数据。

    注意无法接收几个字。

Cookie 不能跨域读写是毋庸置疑的。

document.domain 跨同祖域

如果两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置 document.domain 共享 Cookie(父子框架、窗口)。

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

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

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

HTML5 解决方案:message 机制(CDM)

见下文

DOM

如果两个网页(不同的框架或窗口)不同源,就无法拿到对方的 DOM。典型的例子是 iframe 窗口和 window.open 方法打开的窗口,它们与父窗口无法通信。

为什么要这样呢?
试想以下场景,假如一个钓鱼站点(例如:www.icioud.com),使用 Safari 嵌入了真正的 www.icloud.com 页面,用户在嵌入的框架内输入了账户密码。此时,如果我们可以跨域访问 iframe 内的 DOM,那么这个钓鱼页面就拿到了我们的账户和密码。

对此浏览器及相关规范采取了相关策略:

  1. 可以采用一定的措施防止自己的页面被其他域名的页面使用 iframe 嵌入(具体实现略)
  2. 浏览器同源策略——非同源的 iframewindow.open 打开的窗口不能访问其 DOM

那么如果:

  1. 两个域名都在自己的控制之下
  2. 确保两个域的页面的安全性
  3. 我们确实希望能访问 iframewindow.open 打开的窗口中的 DOM

那么此时我们需要怎么做呢?

document.domain

如果两个窗口 一级域名相同,只是二级域名不同,那么设置上一节介绍的 document.domain 属性,就可以规避同源政策,拿到 DOM。

片段识别符(hash)

image_1bacvor93cc718luddi1qq1c91m.png-522.8kB

iframe 三层:a.com > b.com/data.html > a.com/proxy.html

b.com/data.html 可以通过 Ajax 轮询或其他手段获取数据,数据被 attach 到内嵌 iframe[src=a.com/proxy.html] srchash 上,a.com/proxy.html 监听 hashChange 事件/定时检查(旧浏览器下),a.com/proxy.html 与最外层的 a.com 同域,所以可以正常通信。

window.name

同一窗口或框架加载任何页面 window.name 的值始终保持不变。由于 window.name 这个显著的特点,使其适用于在不同源之间进行跨域通信,但这是个不常用的属性。同时 window.name 可存储的信息达到 2 M,可满足大部分情况下的信息传输。

image_1b6t59ke5vlk1kjm6vtrktv9813.png-28.1kB

当页面 A 想要从另一个源获取资源或 Web 服务,首先在自己的页面上创建一个隐藏的 iframe B(当然新开一个窗口本质上相同),将 B 指向外部资源或服务(例如:otherdomain.com/data.php),B 加载完成之后,将把响应的数据(还是需要 B 所在服务器预先准备好)附加到 window.name 上。由于现在 A 和 B 还不同源,A 依旧不能获取到 B 的 name 属性。当 B 获取到数据之后,再将页面导航(location 变更)到任何一个与 A 同源的页面(例如:domain.com/proxy.html),这时 A 就可以直接获取到 B 的 name 属性值。

当需要拿取最新的数据时,再变更 B 的 src 到 otherdomain.com/data.php 以更新 window.name,再将页面导航到任何一个与 A 同源的页面即可再次通过 window.name 拿到更新的数据。

一般隐藏的 iframe B 在服务器端会类似下面:

  1. <?php
  2. echo '<script> window.name = "{\"name\":\"hanzichi\", \"age\":10}"; </script>'
  3. // echo 的 window.name 可能会频繁更新
  4. ?>

很容易知晓,这个方案存在一定的缺点:

  1. 性能问题
  2. 三个关联的页面(本域页面、外域数据页面、本域 Proxy 页面)都需要自己能完全掌控

HTML5 解决方案:message 机制(CDM)

HTML5 为了解决这个问题(窗口与框架间通信的问题),引入了一个全新的 API:跨文档通信 API(Cross-document messaging, CDM)。

这个 API 为 window 对象新增了一个 window.postMessage 方法,允许跨窗口通信,不论这两个窗口是否同源。

举例来说,父窗口 http://aaa.com 向子窗口 http://bbb.com 发消息,调用 postMessage 方法就可以了。

  1. var popup = window.open('http://bbb.com', 'title');
  2. popup.postMessage('Hello World!', 'http://bbb.com');

postMessage 方法的第一个参数是具体的信息内容第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为 *,表示不限制域名,允许向所有窗口发送。
子窗口向父窗口发送消息的写法类似。

  1. window.opener.postMessage('Nice to see you', 'http://aaa.com');

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

  1. window.addEventListener('message', function(e) {
  2. console.log(e.data);
  3. },false);

message 事件的事件对象 event,提供以下三个属性。

  1. event.source:发送消息的窗口
  2. event.origin: 消息发向的网址
  3. event.data: 消息内容

下面的例子是,子窗口通过 event.source 属性引用父窗口(注意这个引用只有传递消息的功能,没有 window 的其他权限),然后发送消息。

  1. window.addEventListener('message', receiveMessage);
  2. function receiveMessage(event) {
  3. event.source.postMessage('Nice to see you!', '*');
  4. }

event.origin 属性可以过滤不是发给本窗口的消息。

  1. window.addEventListener('message', receiveMessage);
  2. function receiveMessage(event) {
  3. if (event.origin !== 'http://aaa.com') return;
  4. if (event.data === 'Hello World') {
  5. event.source.postMessage('Hello', event.origin);
  6. } else {
  7. console.log(event.data);
  8. }
  9. }

LocalStorage

借助 CDM(message 与 postMessage),我们便可以通过消息的传输与消息事件来间接地实现跨域的 LocalStorage 共享、联动,由此推而广之,Cookie 等的跨域也水到渠成了。

所以 CDM 是终极之道!

AJAX

Ajax 为什么会受到跨域限制呢?

当我们在 A 域下使用 Ajax 向 B 域的某个接口发起请求时,浏览器会为这个 Ajax 请求带上 B 域下的 Cookie,假设刚好该 Cookie 里有登录态,那么此时 B 域所在服务器在没做辨别的情况下就会如实返回数据。假设返回的数据存在一些安全性、私密性的数据,那么 A 域也就拿到了这些数据,此时这些数据就危险了。

所以浏览器为了保护可能的安全、隐私泄露,浏览器在接收到 Ajax 请求的数据之后会进行同源与 CORS(见下文)鉴定,只有符合要求才会将数据正常转交给页面使用。

所以,服务器一般是一视同仁(无法辨别是用户行为请求,还是 Ajax 请求),只要符合要求就返回数据给你,是浏览器为我们的安全考虑了同源策略。即,这些跨域的 AJAX 请求确实是发出去了的,服务器也确实接收到了,但是浏览器收到数据后不给你。

AJAX 请求跨域解决方案 - CORS

在浏览器部署,对于请求的 response 头添加 Access-Control-Allow-Origin 字段指定信任的域。这个字段的主要目的是:告知浏览器我这个 response 对那些域是信任的,在这些域下发起的 AJAX 请求不用拦截数据。

此方案需要在浏览器部署,一般用在专门的 API 服务商。

跨域问题解决方案汇总

  1. CORS
  2. HTML5 message API
  3. JSONP
  4. WebSocket

其他方案

  1. 规避 AJAX,使用 DOM 发起请求,如:图像 Ping
  2. window.name 实现框架/窗口间通信
  3. location hash 实现框架/窗口间通信

参考:http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注