对于绝大多数B/S系统开发者来说,”跨域”并不是什么陌生的词汇,但也往往因为熟悉而忽略其本质。众所周知,Web浏览器往往是不安全的,出于安全考虑,Netscape公司1995年在浏览器中引入同源策略(same-origin policy),目的是为了保证用户信息的安全,防止恶意的网站窃取数据。目前,所有浏览器都实行这个政策,”跨域”问题也由此形成。

所谓同源,简单的讲,就是网址必须是同协议、同域名(主域、子域与不同子域都称为非同域名)、同端口,两个网址出现任一项不同则出现跨域。

受同源策略影响,出现跨域的情况,我们不能做以下三类事

1)不能操作cookie、LocalStorage(SessionStorage) 和 IndexDB
2)不能操作DOM
3)不能发送AJAX 请求

显然,同源策略影响的不止是”恶意网站”,合理的用途也受到影响。下面将介绍如何规避上面三种限制。

一、操作cookie、LocalStorage(SessionStorage) 和 IndexDB

1、cookie

同主域情况下,对于cookie的操作,经常会遇到有同事说某某cookie删不掉等问题,实际多为domain或者path不同所致,对于此类问题,我们只需设置document.domain为根域或在设置cookie时保证域名为根域(如”poorren.com”)且path为根路径”/”即可。对于不同主域,依然是不能规避的,当然,这要排除早些年使用flash脚本恶意抓取第三方网站cookie的方式,不过flash日渐衰败的今天,也可以理解为互联网安全又向前迈了一步。

2、LocalStorage(SessionStorage) 和 IndexDB

使用传统手段无法规避,但HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。每个window下面都有postMessage方法,允许跨窗口通信,不受同源策略影响。通过此API,我们可以简单的在当前页面(如:www.poorren.com)下执行

window.postMessage('data','http://user.poorren.com');

在user.poorren.com添加事件

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

在message事件中,可以获取发送消息的窗口(e.source)、消息发向的网址(e.origin)和消息内容(e.data),需要注意的是,这里data的传输类似LocalStorage(SessionStorage),只能是字符串,不过这毫不影响我们的使用,任何需求只需要发送、接收两端页面代码都是我们所能控制的,就可以轻松实现,具体实现就不再举例,大家可以尽情脑补各种可能性啦。

二、操作DOM

跨域所影响的操作DOM问题即iframe内嵌网站需要进行交互导致,如父页面操作子页面

document.getElementById("iframe").contentWindow.document

或者子页面操作父页面

window.parent.document

都会出现跨域警告,这类情况下如果主窗口与iframe内网址为同根域,则可参考cookie处理方式,设置document.domain为根域,对于完全不同源的网站,有window.name、片段识别符(fragment identifier)、跨文档通信 API(Cross-document messaging)三种方案。

1、window.name

浏览器窗口有window.name属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它,最大可以存储2M的数据,但前提是子窗口写入数据后载入同源页面或者空页面,如

var iframe = document.getElementById('iframe');
var data = '';
iframe.onload = function(){
    iframe.onload = function(){
        data = iframe.contentWindow.name;
    }
    iframe.src = 'about:blank';
};

注:about:blank,javascript: 和 data: 中的内容,继承了载入他们的页面的源。当然,这里两侧页面内都需要对window.name进行监听才能实现数据传递。

2、片段识别符(fragment identifier)

通俗说,其实就是通过location.hash取到的锚点信息,通过window.onhashchange可以监听当前页面location.hash的变化,这点和目前流行的前端路由其实是使用了同一特性,只不过是监听背后做了不同的事。

3、跨文档通信 API(Cross-document messaging)

前两种为早期方案,都不完美,基本思想都是一侧写数据,另外一侧监听,继而实现各种需求。第三种可以说是思想一致,只不过等了这么久终于有了”正规手段”来解决此类问题,具体使用第一段已经提及,这里不多赘述

三、发送AJAX

可能很多朋友第一次遇到跨域问题就是在使用AJAX过程中吧,对于AJAX跨域,有代理、JSONP、WebSocket、CORS四种方案。

1、代理

常见各种服务器语言或者服务器中间件皆可实现,不在前端讨论范围,这里一带而过

2、JSONP

基本思想是,依托script不受同源策略限制的特性,将后端api返回内容包装为js脚本的形式输出(这里需要后端配合变更输出内容),js中通过动态创建script标签,指定src为api地址,待script加载完毕则执行动态插入script中的函数,简单示例如下

function jsonp(src) {
    var script = document.createElement('script');
    script.setAttribute("type","text/javascript");
    script.src = src;
    document.body.appendChild(script);
}

jsonp('http://poorren.com/api?callback=callback');

function callback(data) {
    console.log(data);
};

jQuery等前端工具库都是采用此特性实现,这里不得不提一句,很多初入前端的小朋友认为jQuery的jsonp也可以设置type为post,所以jsonp支持post,相信看到这里就不会这么想了,script标签加载信息,始终都是get请求。

3、WebSocket

WebSocket是一种通信协议,也是HTML5中新增加的一项特性,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。由于目前多数B/S应用并没有大规模使用WebSocket的需求,这里一带而过,不多做介绍。

4、CORS

CORS是一个新的W3C标准,即Cross Origin Resource Sharing(跨来源资源共享),有了CORS,代理、JSONP等方式终将成为过去式。

浏览器将CORS请求分成两类:简单请求(simple request)和复杂请求[非简单请求](not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。

1)请求方法是HEAD、GET、POST三种方法之一
2)HTTP的头信息不超出Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(值只能为application/x-www-form-urlencoded、multipart/form-data、text/plain其中一种)几种字段

任何一个不满足上述要求的请求,即被认为是复杂请求。一个复杂请求不仅有包含通信内容的请求,同时也包含预请求(preflight request)。

简单请求:

简单请求的发送从代码上来看和普通的XHR没太大区别,但是HTTP头当中要求总是包含一个域(Origin)的信息。该域包含协议名、地址以及一个可选的端口。不过这一项实际上由浏览器代为发送,并不是开发者代码可以触及到的。

简单请求的部分响应头及解释如下:

Access-Control-Allow-Origin(必含)- 不可省略,否则请求按失败处理。该项控制数据的可见范围,如果希望数据对任何人都可见,可以填写”*”。

Access-Control-Allow-Credentials(可选) – 该项标志着请求当中是否包含cookies信息,只有一个可选值:true(必为小写)。如果不包含cookies,请略去该项,而不是填写false。这一项与XmlHttpRequest2对象当中的withCredentials属性应保持一致,即withCredentials为true时该项也为true;withCredentials为false时,省略该项不写。反之则导致请求失败。

Access-Control-Expose-Headers(可选) – 该项确定XmlHttpRequest2对象当中getResponseHeader()方法所能获得的额外信息。通常情况下,getResponseHeader()方法只能获得Cache-Control、Content-Language、Content-Type、Expires、Last-Modified和Pragma。

上面说到,CORS请求默认不发送cookie和HTTP认证信息。如有需要,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

Access-Control-Allow-Credentials: true

另一方面,必须在AJAX请求中打开withCredentials属性。

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器同意发送cookie,浏览器也不会发送。或者,服务器要求设置cookie,浏览器也不会处理。但如果省略withCredentials设置,有的浏览器还是会一起发送cookie。所以可以显式关闭withCredentials。

xhr.withCredentials = false;

需要注意的是,如果要发送cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,cookie依然遵循同源政策,只有用服务器域名设置的cookie才会上传,其他域名的cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的cookie。

复杂请求:

复杂请求表面上看起来和简单请求使用上差不多,但实际上浏览器发送了不止一个请求。其中最先发送的是一种”预请求”,此时作为服务端,也需要返回”预回应”作为响应。预请求实际上是对服务端的一种权限请求,只有当预请求成功返回,实际请求才开始执行。预请求以OPTIONS形式发送,请求头在简单请求的基础上增加了两项CORS特有的内容:

1)Access-Control-Request-Method – 该项内容是实际请求的种类,可以是GET、POST之类的简单请求,也可以是PUT、DELETE等等。
2)Access-Control-Request-Headers – 该项是一个以逗号分隔的列表,当中是复杂请求所使用的头部。

显而易见,这个预请求实际上就是在为之后的实际请求发送一个权限请求,在预回应返回的内容当中,服务端应当对这两项进行回复,以让浏览器确定请求是否能够成功完成。如上所述,假如请求时传入

Access-Control-Request-Method: PUT
Access-Control-Request-Method: token

响应头

Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: token

则预请求成功,否则失败

复杂请求的部分响应头及解释如下:

Access-Control-Allow-Origin(必含) – 和简单请求一样的,必须包含一个域,但不能为”*”。

Access-Control-Allow-Methods(必含) – 这是对预请求当中Access-Control-Request-Method的回复,这一回复将是一个以逗号分隔的列表。尽管客户端或许只请求某一方法,但服务端仍然可以返回所有允许的方法,以便客户端将其缓存。

Access-Control-Allow-Headers(当预请求中包含Access-Control-Request-Headers时必须包含) – 这是对预请求当中Access-Control-Request-Headers的回复,和上面一样是以逗号分隔的列表,可以返回所有支持的头部。这里在实际使用中有遇到,所有支持的头部一时可能不能完全写出来,而又不想在这一层做过多的判断,没关系,事实上通过request的header可以直接取到Access-Control-Request-Headers,直接把对应的value设置到Access-Control-Allow-Headers即可。

Access-Control-Allow-Credentials(可选) – 和简单请求当中作用相同。

Access-Control-Max-Age(可选) – 以秒为单位的缓存时间。预请求的的发送并非免费午餐,允许时应当尽可能合理的缓存,不过这里有网友实测设置超过15分钟的缓存,时段内依然会再次预请求,猜测可能是浏览器bug等因素,这里没做过多追溯。

一旦预回应如期而至,所请求的权限也都已满足,则实际请求开始发送。

通http://caniuse.com/#search=cors得知,目前大部分Modern浏览器已经支持完整的CORS,但IE直到IE11才完美支持,所以对于PC网站,如果考虑低版本IE,还是建议优先采用其他解决方案,如果仅仅是移动端网站,大可放心使用。

注:之前整理过关于CORS的文章《CORS跨域请求[简单请求与复杂请求]》,最近应公司征集前端相关的文章的要求把其他跨域解决方案大概整理一下,因时间仓促,网上收集资料参考整理后输出此文,也顺便在博客发一下。文章资源整理自网络,如有版权问题请联系,我会及时移除。