同源政策与跨域详解

跨域的应用场景

在 Web2.0 时代,许多应用需要聚合信息(比如来自google, 来自wikipedia的内容),因此来自A站点的网页,往往需要读取来自B的内容,这是受到同源策略约束的。

什么是同源政策(same-origin policy)

  • 协议相同(FTP、HTTP等)
  • 域名相同(包括每一级域名, foo.comwww.foo.com不同)
  • 端口相同

以下行为受到限制(通常是跨域读操作):

  1. Cookie、LocalStorage、IndexDB 等存储性内容
  2. DOM 节点
  3. AJAX 请求不能发送

为什么需要同源政策

保护用户隐私信息,防止身份伪造等(读取Cookie)

跨域写操作与跨域嵌入操作一般不受到约束。具体参考MDN

非AJAX的跨站请求

document.domain 共享 DOM 与 存储

对于只有前缀(二级、三级等域名)不同的网页,可以设置 document.domian 来规避同源策略

1
2
3
4
5
6
//对于 s1.a.com 和 s2.a.com,设置以下内容可以获取cookie
document.domain = 'a.com'
//document.domain 必须是域名的后缀,对于s1.a.com
document.domain = 'a.com' //Right!
document.domain = 'b.com' //Error! 'b.com' is not a suffix of 'a.com'

具有相同domian的可以互相读取Cookie:

1
2
3
4
5
6
// s1.a.com
document.cookie = "test1=hello";
// s2.a.com
var allCookie = document.cookie;
//服务器设定cookie的domain
Set-Cookie: key=value; domain=.example.com; path=/

也可以读取iframe内的DOM节点

fragment identifier 共享 DOM

fragment identifier 指的是URL中hash符号#后面的内容,不引起页面刷新

父窗口可以改变子窗口的fragment identifier,反之亦然

1
2
3
4
5
6
7
8
//改变子窗口的url
var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;
//子窗口响应事件
window.onhashchange = function () {}
//子窗口改变父窗口的hash
parent.location.href= target + "#" + hash;

window.name

window.name最早是用来规避cookie缺点设立的(cookie过小, API复杂),window支持2MB以上大小

name是window的一个属性,无论window的内容如何改变其值不发生变化,因此利用iframe标签页和window.name可以实现跨域

首先要简单了解iframe的相关知识,iframe在网页中创建了一个内联框架,通过src属性指向其他网站,每一个iframe都有一个包裹他的window,他是主窗口的子窗口

由此,跨站方案就非常简单了,我们首先声明一个iframe标签指向跨站的网站,在跨站的页面内设置window.name,当检测到onload(只运行一次)以后把src设置回同域站点然后读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let iframe = document.createElement('iframe');
iframe.style.display = 'none';
let state = 0; //用state控制onload只运行一次,避免来回刷新
iframe.onload = function() {
if (state === 0) {
state = 1;
iframe.contentWindow.location = '同源域名'
} else if (state === 1) {
let data = JSON.parse(iframe.contentWindow.name);
//... do something with data
document.body.removeChild(iframe)
}
}
iframe.src = '跨域站点';
document.body.appendChild(iframe)

这一解决方法的缺点主要在于需要使用iframe并且监听子窗口,影响了网页的性能

window.postMessage

HTML5中引入, 用于跨域的父子窗口通信,不受同源策略限制.
通过postMessage API可以实现对存储的读写,DOM的操作等

1
2
3
4
5
6
7
8
9
var popup = window.open('http://bbb.com', 'title');
//父窗口向子窗口发送消息
popup.postMessage('Hello World!', 'http://bbb.com');
//子窗口向父窗口发送消息
window.opener.postMessage('Nice to see you', 'http://aaa.com');
//父子都可以监听message事件响应
window.addEventListener('message', function(e) {
console.log(e.data);
},false);

AJAX跨域

WebSocket

WebSocket通信协议不实行同源政策

JSONP

JSONP优点是兼容性好,缺点是仅支持get方法具有局限性

其设计思路是因为浏览器不对 <script> 标签进行限制,因此可以利用这一点来进行跨域请求。

  1. 声明一个回调函数,其参数为要获取(服务器提供的data),对参数进行操作(比如渲染进DOM)
  2. create一个<script>标签动态加入DOM tree,在src的URL中向服务器传递该函数名
  3. 服务器返回一个js脚本文件,将数据包括在url中给的回调函数里,运行回调函数

CORS

阮一峰的文章写的非常清晰易懂

CORS要求浏览器(>IE10)和服务器的同时支持,是跨域的根本解决方法,由浏览器自动完成
优点在于功能更加强大支持各种HTTP Method,缺点是兼容性不如JSONP

简单请求

定义

满足以下全部2个条件的就是简单请求,否则是非简单请求

  1. 使用以下三个方法之一: GET、POST、HEAD
  2. HTTP头不超过以下几个字段
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

流程

浏览器将在请求头加入origin字段指定源,如果服务器支持并且该源在白名单里,将返回一个包含特殊头字段的响应,否则不包含这些特殊头字段,XMLHttpRequest 可以捕获错误,但是响应状态代码依然会是200

多出的字段:

  1. Access-Control-Allow-Origin(必须): 值为 * 或者请求的origin
  2. Access-Control-Allow-Credentials(可选): 值只能为true表明发送cookie,默认不发送cookie不包含该字段,需要设置xhr.withCredentials = true;
  3. Access-Control-Expose-Headers(可选): XMLHttpRequest.getResponseHeader()方法只可以获取Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma六个基本字段和该头字段指定的字段名的值

非简单请求

预检请求(Preflight)

对于非简单请求(非基本MethodHeader,需要首先发送一个OPTIONS请求询问服务器是否支持
浏览器根据AJAX的请求Method和Header,自动加入字段:

  1. Origin: 跨域必须指定的Origin
  2. Access-Control-Request-Method: 指定了请求要用到的方法,比如PUT
  3. Access-Control-Request-Headers(可选): 一个逗号分隔的字符串,指定了可能的额外Header字段

服务器检查自身是否支持后,进行回应:

  • 否定预见请求: 返回没有任何CORS字段(Access-Control)的正常响应,可以用XMLHTTPRequest对象的 onerror 捕捉处理
  • 肯定预见请求:
    1. Access-Control-Allow-Methods: GET, POST, PUT (回复支持的方法)
    2. Access-Control-Allow-Headers(如果请求具有): 逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
    3. Access-Control-Allow-Credentials: 是否传递cookie,同简单请求
    4. Access-Control-Max-Age(可选): 表面预检的有效期,有效期内不发送预检请求
    

正常请求

在预检请求之后,正常请求与简单请求类似,请求具有Origin段,回复具有Access-Control-Allow-Origin等字段。