1、为何须要JS跨域
假设咱们构建了一个网上商城www.xxx.com,出于对用户帐号安全性的考虑,咱们将用户登陆统一到auth.xxx.com的子域下验证。当一个未登陆用户浏览商品之后点击购买,为了提升网站的用户体验,咱们想提供一个无刷新的登陆入口。咱们马上想到使用AJAX实现无刷新的数据交互,可当咱们实际使用AJAX向auth.xxx.com提交数据的时候,JS却出现错误提示,咱们没有权限进行此操做,由于XMLHTTPRequest的实现要遵循浏览器的安全模型的同源策略规则,JS只能往本身的同源(同协议、同域名、同端口)发送XMLHTTPRequest请求,因此咱们没法往子域发送AJAX 请求。
解决此问题的一般作法是由auth.xxx.com提供接口,在www.xxx.com的域下作一个服务端代理。但随着业务的发展,咱们又有 a.xxx.com,b.xxx.com...等等都须要到auth.xxx.com作登陆验证,那么咱们须要在每个子域下都有一个服务端的 Proxy,值得庆幸的是这些子域都是咱们本身的,对服务器上的程序有控制权,因此操做起来仍是可行的。然而随着业务的继续发展,咱们有愈来愈多的合做伙伴,如今咱们要与他们实现部分数据的跨域互通,那么咱们就须要和他们去约定一大堆的接口,而后本身逐一去实现Proxy,那么这种方式会带来很大的沟通和维护成本。有没有更好的方式呢?有!那就是客户端的JS跨域方案。
2、浏览器端的JS跨域方案
不管是子域仍是非子域,JAVASCRIPT自己并无官方的跨域实现,下面介绍的跨域方案都是开发者创造出来的巧妙的hack实现:
一、设置document.domain跨子域
二、使用iframe的hash
三、使用script标签
四、flash转发
上面各类方案的适用场景和开发成本也是不一样的,你能够根据具体的状况来选择合适的跨域方案。
一、设置document.domain
若是仅仅是要跨子域,那么最简单的方式是设置document.domain,再配合隐藏的iframe,而后经过下面的方式来完成异步请求。
1.1 使用FORM提交
早期浏览器并不支持XMLHTTPRequest时,开发者就已经使用隐藏iframe和form的方式来作同源的异步数据提交,原理就是iframe间的相互通讯。虽然咱们如今是在子域间作异步数据提交,但经过设置domain咱们已经获取了子域iframe之间的控制权(须要注意的是并无直接使用 XMLHTTPRequest往auth子域下的页面发送异步请求的权限),那么下面要作的工做和之前没有区别,这里就大概描述一下,更具体的能够搜索下使用iframe作异步提交的资源。
咱们在A页面(http://www.xxx.com/index.html)中以iframe形式插入一个auth子域下的B页面(http: //auth.xxx.com/proxy.html),并设置index.html和proxy.html页面的domain为xxx.com,这样 index.html和proxy.html就有了互相访问的权限。
当用户在index.html输入登陆信息提交时,咱们能够获取到用户输入的用户名和密码,而后咱们把此信息写入到proxy.html页面中。一般咱们会在proxy页面中放置一个form,而后把须要提交的数据写入到此form中再提交此form,这样就完成了www到auth的数据提交。那咱们怎么知道用户是否登陆成功呢,咱们须要一个反馈。proxy中的form对应的action页面接受到登陆信息之后作验证,而后将反馈信息以JS脚本的形式输出,例如document.domain='xxx.com'; top.showLogin({'code':'0','user':'yoyo'});,这样若是咱们在index页面中定义了showLogin函数,那么用户登陆之后就会自动调用,而且接收都到了auth的登陆反馈。
1.2 使用XMLHTTPRequest提交
刚才咱们是把数据经过proxy页面中的FORM提交出去的,实际上如今的主流浏览器都已经支持XMLHTTPRequest,咱们彻底能够控制 proxy页面实例化一个XMLHTTPRequest对象,那么这个实例是在proxy的域下的,它是能够往auth.xxx.com提交AJAX请求的。这和咱们页面自己发送AJAX请求所不一样的仅仅在于操做的window对象不一样,若是咱们把这个差别封装起来,那使用者也是感觉不到其中的差别的。
二、使用iframe的hash 同刚才的作法咱们在A页面(http://www.xxx.com/index.html)中以iframe形式插入一个auth子域下的B页面 (http://auth.xxx.com/proxy.html),但此次咱们不须要设置document.domain,受安全策略的限制A页面的 JS没法获取到A页面中iframe的src属性,更没有办法访问页面B中的元素,但它能够设置iframe的src,假如A页面输入的用户名和密码同时以必定格式加入到了src对应的url中,例如:http://auth.xxx.com/proxy.html?user=yoyo& pwd=123456,B页面是能够在GET信息中获取此登陆信息作验证的。这就完成了A到B的SET操做。那B登陆验证之后怎么给A反馈呢?同理,咱们能够在B页面以iframe形式引入一个www下的A_proxy页面(http://www.xxx.com/proxy.html),那么它也能够经过设置iframe的src的方式实现B到A_proxy的SET操做,而A_proxy和A是一个域,他们之间就能够自由地完成通讯了。
若是咱们把参数以GET的形式设置到iframe的src中,会形成iframe频繁发送请求。为了不这点,咱们能够把参数以锚点的形式传入进入,同时在iframe里对锚点的改变作一个监听便可。那如何监听锚点的改变呢?
2.1:能够在iframe中使用setInterval频繁检查location.hash是否改变。若是你对你的代码效率有洁癖,那么你能够考虑使用下面的方法。
2.2:在设置iframe的src的同时,改变一下iframe的width属性,iframe内部使用onresize事件获知hash的改变。
此方法不局限于跨子域的需求,在FaceBook的APP平台中,第三方应用的iframe的高度自适应就是采用此方法实现的(实际上iframe高度自适应没有A到B的set需求,仅仅是B到A的set需求)。须要注意的是此方法的弊端在于作SET操做的时候受制于各个浏览器对url长度的限制,没法发送过多的数据。
三、使用script标签
当咱们以script标签的src属性引入一个JS文件到页面中时,咱们须要清楚下面两条规则:
3.一、文件下载完毕之后,其中的JS会自动运行。
3.二、不管src来源的域是什么,这段JS执行时信任的域都是此页面的域。
当用户登陆的时候,A页面使用JS动态添加一个script节点到页面中,而且把用户名和密码设置到src属性中,例如:http://auth.xxx.com/login?user=yoyo&password=123456,那么完成了A到B的SET操做,当B作完验证之后,把结果以JS形式返回过来,例如:showLogin({'code':'0','user':'yoyo'})。根据规则一这个 JS函数会自动执行,若是咱们在页面A中定义了showLogin的函数,这就实现了B到A的回调反馈。
此方法的经典应用就是JSONP,它在此核心功能的基础上有定义了一些规则:例如返回值都是JSON格式,script标签的src中引入一个 callback参数指定回调函数名。例如flickr的API,http://api.flickr.com/services/feeds /photos_public.gne?tags=cat&tagmode=any&format=json& jsoncallback=doSomething,返回的结果就是doSomething({JSON格式}),任何网站均可以经过JSONP调用此 API来展现此照片列表。
在Jquery中也有getScript和getJSON的封装,它隐藏了script标签和回调函数的动态建立、垃圾回收、非跨域状况下的浏览器阻塞(具体细节的实现也是颇有意思的,有空我会单独写一个分析),使用者只须要处理数据和回调函数便可。
此方法不局限于跨子域的需求,但因为它也是依赖设置script标签的src属性来作的set操做,因此它一样受制于各个浏览器对url长度的限制,没法发送过多的数据。
四、flash转发
ACTIONSCRIPT自己也有发送异步请求的实现,虽然FlashPlayer也有安全域限制,但相对于JS而言彷佛更人性化。最初我由AS转入JS 的时候就很诧异,一样是基于ECMAScript的实现,为什么JS没有官方的跨域机制呢?首先简单讲一下FlashPlayer沙箱机制中的咱们会用到的三个主要原则:
4.一、FlashPlayer发送请求的时候也须要遵循浏览器的同源策略,在早期版本中子域被视为安全域,但随着安全级别的提升,目前遵循的同源策略和JS同样,只能往本身的同源(同协议、同域名、同端口)发送请求。若是须要往第三方的域发送请求,那么须要第三方在本身的server上放置一个 crossdomain.xml的XML文件。此文件就比如是一个白名单,能够设置本身信任的第三方域的访问。
4.二、A域的页面引入了一个B域的Flash,此Flash是没法和页面中的JS通讯的,须要A域页面在插入Flash的时候设置allowscriptaccess属性为B域(flash所在的域),或者是always(开放全部域)。
4.三、A域的页面中的JS也没法访问B域的Flash,须要Flash在脚本中经过allowDomain设置了A域为本身信任的安全域。
了解了上面三个原则之后,咱们就能够解决A域页面和B域的flash的互相通讯的问题,B域flash和A域的server通讯的问题。下面介绍下Flash转发的实现原理:
用户交互仍是由JS完成,当咱们须要跨域发送请求的时候,咱们可使用JS把数据传递给FLASH,而后由FLASH去跨域请求server,而后把 server处理完毕的结果传递给js。具体实现主要是网络通讯和回调函数,须要特别注意的是FLASH和JS通讯的时候,字符串中若是出现"\",那么须要在AS内部对"\"作一次转义,不然到了JS端此"\"会被看成转义符解析掉。
若是你是一个server端的程序员,你可能已经有一个疑惑了:真实的请求是由B域的flash发送的,若是咱们的这个请求须要A域下的cookie信息怎么办呢?这里就须要提到一个FlashPlayer的奇妙"特性",不管flash的域在哪儿,只要A域的server容许此域的flash访问,它向 A域发送请求的时候,都会把此时浏览器中属于A域的cookie一同发送过去,所以"理论"上你不须要担忧这个问题,但我须要提醒两点:
1、这个特性仅限于URLRequest类,AS中的文件上传类就没有此特性,相反它的实现让人感受是残缺的,见"当心SWFUpload的cookie Bug"。
2、在我项目中我曾经遇到过URLRequest类也丢失cookie的状况,但后来一直没有再重现,我也没法排除当时有其余因素的影响,因此我刚才加了"理论"二字。若是你遇到了这个状况,也能够给我一个反馈,一块儿交流一下。如今我为了保险起见,我仍是主动用JS把cookie获取,而后加入到 flash的请求中。
在Facebook的APP平台中有一个Fb:local-proxy的实现,实际上核心就是Flash转发,它的优点在于跨域没有局限,SET和GET操做也没有数据长度的局限,劣势在于客户端须要依赖FLASH,须要考虑的客户端复杂性。
测试环境部署麻烦,并且空余时间也有限,因此没有去写具体的示例代码,上面写得仍是很粗,见谅。细节问题能够邮件、IM交流。