首页 > 上网技巧 > 电脑小技巧 > Web开发之跨域与跨域资源共享

Web开发之跨域与跨域资源共享

时间:2016-12-08 15:05 作者:QQ地带 我要评论

同源策略(same origin policy)
1995年,同源政策由 Netscape 公司引入浏览器。为了防止某些文档或脚本加载别的域下的未知内容,防止造成泄露隐私,破坏系统等行为发生。
 
同源策略做了两种限制:
 
不能通过ajax的方法或其他脚本中的请求去访问不同源中的文档。
浏览器中不同域的框架之间是不能进行js的交互操作的。
现在所有的可支持javascript的浏览器都会使用这个策略。
 
怎么算同源
URL的三部分完全相同时我们就可以称其为同源,这三部分是: 协议,域名(主机名)和端口都相同。
 
IE 例外
当涉及到同源策略时,Internet Explorer有两个主要的例外
 
授信范围(Trust Zones):两个相互之间高度互信的域名,如公司域名(corporate domains),不遵守同源策略的限制。
 
端口:IE未将端口号加入到同源策略的组成部分之中,因此 http://company.com:81/index.html 和http://company.com/index.html  属于同源并且不受任何限制。
跨域的几种解决方法
虽然同源策略很有必要,但有很多时候我们还是需要去请求其他域的数据,如:调用不同业务的数据,而不同业务已子域区分;又或者是第三方公用的数据接口等等
 
由于各种原因,我们需要通过各种方式来请求到不同域下的资源。
 
jsonp
jsonp是通过可以发出跨域请求的script标签,使javascript能够获得跨域请求的数据,并调用数据。
 
先看个例子:
文件index.js :
 
 
  1. alert(123); 
 
页面index.html:
 
 
  1. <script src="./index.js"></script> 
 
当加载页面index.html后,出123内容的弹窗。通过查看index.js的响应体,会发现响应内容就是alert(123)。
 
 
 
所以,可以这么思考,只要是通过script标签请求到的内容就会被当做js代码执行。
 
是否可以在script中的地址src不请求js文件,而是请求服务端的接口(即使不在同源下的),那么返回的内容就能获得到,并且会当成js代码来执行。(一般的script标签都会去请求js代码文件)
 
再来看下正常的服务端获取数据接口。
 
比如:有这么个接口/getUserInfo/001,通过ajax请求获得此接口数据{"data" : {"name" : "oicqzone",like:"everything"}}。
得到数据后在ajax中调用showUserInfo(data)来渲染页面,data就是接口数据。
如果现在用script标签来请求数据,那么同样可以获得数据,执行返回到的内容,因是json格式的数据,并不会报错,但也并没有卵用。获得接口的数据肯定是想做些什么的。
 
再想想,正常ajax请求后的js执行内容showUserInfo(data),拿到数据后,调用了showUserInfo函数。
 
那么,用script标签来请求数据时,返回的内容直接是showUserInfo(data)不就行了,但服务端又不知道我们到底要执行哪个函数,即使事先约定了,但后面因某些事要改,那还得告诉服务端,太麻烦了。
如果知道要执行什么函数就好了。
 
当然,这是可以的,改造下接口,以参数的形式把函数名传给服务端。
 
 
  1. <script src="/jsonp/getUserInfo/001?jsonp_fn=showUserInfo"></script> 
 
Response返回的内容同样需要改造
 
 
  1. Response: 
  2.     showUserInfo({"data" : {"name" : "oicqzone",like:"everything"}}) 
 
 
这样,通过jsonp,去跨域请求接口数据就完成了。
需要注意的是函数名需要挂在window下面,要不然会报函数名未定义。
 
改变源(origin):通过document.domain与子域之间的跨域通讯
例如在demo.oicqzone.com/index.html页面里执行如下内容:
 
document.domain = 'oicqzone.com';
执行该语句后,可以成功通过oicqzone.com/index.html的同源检测, 实现数据的通讯,
当然document.domain不能随意设置,只能设置成当前域,或设置成当前域的顶域。
 
document.domain常常被用于同站但不同域的情况,例如:www.oicqzone.com,下嵌入了iframe广告页面ad.oicqzone.com,想要实现两页面的通讯,就需要对两个页面都设置document.domain='oicqzone.com'。
 
window.name
window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。
 
name只能是字符串。
 
页面a.html中:
 
  1. <script> 
  2. window.name = 'page name index.html'
  3. setTimeout(function(){ 
  4.     window.location = 'http://localhost:8080/static/b.html'
  5. }, 2000); 
  6. </script> 
 
页面b.html:
 
  1. <script> 
  2.     alert(window.name); 
  3. </script> 
 
 
再来看看如何让a.html页面获取数据
 
用data.html作为请求数据地址:
 
  1. <!DOCTYPE html> 
  2. <html lang="en"> 
  3. <head> 
  4.     <meta charset="UTF-8"> 
  5.     <title>Data</title> 
  6. </head> 
  7. <body> 
  8.     <script> 
  9.     window.name = '{ "name":"oicqzone","like" : "everything"}'; // 需要传入 a.html页面的数据,必须是字符串 
  10.     </script> 
  11. </body> 
  12. </html> 
 
 
a.html:
 
  1. ... 
  2. <iframe src="http://localhost:8080/static/data.html" onload="getData();" frameborder="0" id="iframe_1"></iframe> 
  3. <script> 
  4. function getData(){ 
  5.     var iframe = document.getElementById('iframe_1'); 
  6.     //隐藏iframe 
  7.     iframe.setAttribute("width", "0"); 
  8.     iframe.setAttribute("height", "0"); 
  9.     iframe.setAttribute("border", "0"); 
  10.     iframe.setAttribute("style", "width: 0; height: 0; border: none;"); 
  11.     iframe.onload = function(){ 
  12.         console.log(iframe.contentWindow.name); 
  13.         var data = iframe.contentWindow.name; 
  14.         data = JSON.parse(data);//转成 JSON 
  15.         showUserInfo(data);  
  16.     } 
  17.     iframe.src = 'about:blank'
  18. function showUserInfo(data){ 
  19.     console.log(data); 
  20.     // .....do something 
  21. </script> 
  22. ... 
 
 
当访问http://127.0.0.1:8080/static/index.html,便能获得来自不同域下data.html中的数据。
 
也可以做的更完善些,动态的生成iframe请求数据,用完即毁。
 
 
  1. .... 
  2. // 传入请求数据接口地址和回调函数 
  3. function requestData(url,successCB){ 
  4.     var body = document.getElementsByTagName('body')[0]; 
  5.     var iframe = document.createElement("iframe"); 
  6.     iframe.setAttribute("id", "getDataByWindowName"); 
  7.     iframe.setAttribute("width", "0"); 
  8.     iframe.setAttribute("height", "0"); 
  9.     iframe.setAttribute("border", "0"); 
  10.     iframe.setAttribute("style", "width: 0; height: 0; border: none;"); 
  11.     iframe.setAttribute("src", url); 
  12.     body.appendChild(iframe); 
  13.     setTimeout(function(){//防止iframe.src在没加载前就被替换 
  14.         iframe.onload = function(){ 
  15.             var data = iframe.contentWindow.name; 
  16.             if(data){ 
  17.                 data = JSON.parse(data);//转成 JSON 
  18.                 successCB && successCB(data); 
  19.             } 
  20.             iframe.parentNode.removeChild(iframe); 
  21.         } 
  22.         iframe.src = 'about:blank'
  23.     }, 100); 
  24.  
  25. //requestData("http://localhost:8080/static/data.html",showUserInfo); 
  26. ... 
 
 
这就是使用window.name来进行跨域。
 
window.postMessage
window.postMessage方法是html5的新特性之一,
可以使用它来向其它的window对象发送消息,不管这个window对象是属于同源或不同源。
 
通过window.postMessage允许浏览器windows, tabs, and iFrames之间跨域通讯。
 
之前写过一篇关于window.postMessage的,做了详细的说明+演示页面+演示代码,去看看
 
服务端地址映射
例如一个网站上有各种不同的业务,不同的业务有其对应的子域。
 
如:ad.oicqzone.com;upload.oicqzonecom;live.oicqzone.com,分别对应广告业务,上传业务,直播业务。
 
想在www.oicqzone.com中做交互,或获得数据,便会受跨域影响。
 
造成跨域的原因是因为请求数据的源不同,那只要请求的源一样,便没有跨域问题了。
 
这也是可以办到的,只需要web服务做下代理,或称之为地址映射。
 
拿Nginx举例,需要在web服务上做如下配置:
 
  1. ... 
  2. lcaotion /ad { 
  3.     proxy_pass http://ad.oicqzone.com 
  4.  
  5. location /upload { 
  6.     proxy_pass http://upload.oicqzone.com 
  7.  
  8. location /live { 
  9.     proxy_pass http://live.oicqzone.com 
  10. ... 
 
 
然后就可以在以www.oicqzonecom/ad/的方式去调用广告业务。
 
CORS跨域资源共享
当一个发起的请求地址与发起该请求本身所在的地址不在同源下时,称该请求发起了一个跨域的HTTP请求。
 
有些的跨域请求是被允许的<img>,<script>,<link>图片,脚本,样式及其他资源 ,加载这些数据时即使不在同源下面也同样被允许,如今的网站通常也会去引用不在同源下的这些资源,如做CDN加速。
 
但也有些不被允许,正如大家所知,出于安全考虑,浏览器会“限制”脚本中发起的跨站请求,比如:XmlHttpRequest。
 
除了XmlHttpRequest外,还有以下几种跨域请求做了相应的安全限制。
 
比如:
 
1 前面说的iframe,通过设置src可以发起跨域请求,但对请求到的内容进行操作就不被允许了。如执行iframe.contentWindow.name就会报错。
 
2 <img>标签上crossorigin属性是一个CORS的配置属性,目的是为了允许第三方网站上的图片(即不在同源上的图片)能够在canvas中被使用。
如果没有配置改属性,又跨域请求了图片,当调用canvas中的toBlob(), toDataURL(), 或getImageData()方法的时候,会报错。
 
  1. var img = new Image, 
  2.     canvas = document.createElement("canvas"), 
  3.     ctx = canvas.getContext("2d"), 
  4.     src = "https://sf-sponsor.b0.upaiyun.com/45751d8fcd71e4a16c218e0daa265704.png"; // insert image url here 
  5. img.crossOrigin = "Anonymous"
  6.  
  7. img.onload = function() { 
  8.     console.log(img); 
  9.     canvas.width = img.width; 
  10.     canvas.height = img.height; 
  11.     ctx.drawImage( img, 0, 0 ); 
  12.     localStorage.setItem( "savedImageData", canvas.toDataURL("image/png") ); 
  13. img.src = src; 
3 同样的script也可以有crossorigin属性,
script本身没有跨域问题,不然jsonp就没法用了。但如果请求的不是同源下的js文件,发生错误后,无法通过window.onerror事件捕捉到详细的信息
 
例如加载index.js文件,其中a未定义:
 
 
var b = a;
同源下的 window.onerror报错信息
 
 
 
跨域下的 window.onerror报错信息
 
 
 
通过script标签上添加crossdomain属性,并在服务上配置响应头。
 
 
<script src="http://lcoalhost:/static/index.js" type="text/javascript" charset="utf-8" crossdomain></script>
在去看onerror中的报错信息就和同源下的报错信息一样了。
 
4 Web字体 (CSS 中通过 @font-face 使用跨站字体资源),使用非同源地址,同样会报错。
 
还需要注意的一点是,跨域请求并非是浏览器限制了请求,而是浏览器拦截了返回结果。不管是否跨域,请求都会发送到服务端。
但也有特例,有些浏览器不允许从HTTPS的域跨域访问HTTP,比如chrome_6494_1.html' target='_blank'>ChromeFirefox,这些浏览器在请求还未发出的时候就会拦截请求。
 
解决这类跨域问题的方法就是*CORS*,对于简单的请求来说,前端这边都不需要做任何的编码就能实现跨域请求,
只需要服务端配置响应头”Access-Control-Allow-Origin:*”。
 
什么是CORS
 
CORS是一个W3C标准,全称“跨域资源共享”(Cross-origin resource sharing)
 
跨源资源共享标准通过新增一系列 HTTP 头,让服务器能声明哪些来源可以通过浏览器访问该服务器上的资源。
 
CORS服务端设置(Set Response Header)
Access-Control-Allow-Origin
 
根据Reuqest请求头中的Origin来判断该请求的资源是否可以被共享。
 
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(该字段的值为服务端设置Access-Control-Allow-Origin的值)便知出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。此时HTTP的返回码为200,所以 这种错误无法通过状态码识别。
 
Access-Control-Allow-Credentials
 
指定是否允许请求带上cookies,HTTP authentication,client-side SSL certificates等消息。
如需要带上这些信息,Access-Control-Allow-Credentials:true并需要在XmlHpptRequest中设置xhr.withCredentials=true。
 
需注意的是,当设置了the credentials flag为true,那么Access-Control-Allow-Origin就不能使用”*“
 
Access-Control-Max-Age
 
可选字段,指定了一个预请求将缓存多久,在缓存失效前将不会再发送预请求。
 
Access-Control-Allow-Methods
 
作为预请求Response的一部分,指定了真实请求可以使用的请求方式。
 
Access-Control-Allow-Headers
 
作为预请求Response的一部分,指定了真实请求可以使用的请求头名称(header field names)。
 
CORS两种请求方式
CORS的有两种请求方式: 简单请求(Simple Request) 和 预请求(Prefilght Request)
 
简单请求(Simple Request)
只要同时满足以下三大条件,就属于简单请求。
 
a) 请求方式是以下几种方式之一
 
* GET
* POST
* HEAD
b) content-type必须是以下几种之一
 
* application/x-www-form-urlencoded
* multipart/form-data
* text/plain
c) 不会使用自定义请求头(类似于 X-Modified 这种)。
 
预请求(Prefilght Request)
如果不满足简单请求的三大条件,会在发送正真的请求前,发送个请求方式为’OPTIONS’的请求,去服务端做检测,
 
1) 请求方式不是GET,POST,HEAD
 
那么需要在响应HEAD配置允许的请求方式,例如:Access-Control-Allow-Methods:PUT,DELETE
 
2) 使用自定义请求头,如x-oicqzone ,那么需要在服务端相应配置允许的自定义请求头:Access-Control-Request-Headers: x-oicqzone
 
一旦检测不通过,浏览器就会提示相应的报错,并不会发生真实的请求。
 
CORS兼容性
 
 
从上图可只IE11,以下的就不支持CORS了。但实际上再IE8,IE9,IE10中,可以用XDomainRequest对象代替XmlHttpReuqest,发送跨域请求。
 
  1. var xdr = new XDomainRequest();  
  2.  
  3. xdr.open("get", "http://www.oicqzone.com/xdr"); 
  4.  
  5. xdr.send(); 
 
结语
最后,总结下各种跨域方案的特点,还记得本文开始说的,同源策略的两种限制吗?
 
不能通过ajax的方法或其他脚本中的请求去访问不同源中的文档。
浏览器中不同域的框架之间是不能进行js的交互操作的。
把第1种标记为TYPE_1,第二种标记为TYPE_2,对上述的几种解决跨域的方法分下类。
 
window.name 需要注意name只能是字符串
 
解决的限制 :TYPE_1,TYPE_2
 
缺点: 接口返回的内容必须都是html里嵌入script脚本。
 
document.domain 通过修改domain跨子域
 
解决的限制 :TYPE_2
 
缺点: 仅支持同个域下的子域跨域,跨域能力有限
 
window.postMessage 用于iframe、window、tabs之间的跨域通讯
 
解决的限制 :TYPE_2
 
缺点: 兼容问题,IE10以下受限,IE8以下无效
 
jsonp 是之前最常用的解决跨域请求的方法。
 
解决的限制 :TYPE_1
 
缺点: 不能用于POST请求
 
服务端地址映射 前端不需要管,并能解决跨域请求问题的一种方法。
 
解决的限制 :TYPE_1
 
缺点: 非要说缺点,那就是要说服服务端同学,而且一般场子铺大了的公司只用同源,不太可能。
 
CORS 感觉目前比较常用的解决跨域请求的方法。
 
解决的限制 :TYPE_1
 
缺点: 也是兼容性问题
 
真正开发过程中,需针对不同情况,使用不同的解决之法。

标签: web
顶一下
(0)
0%
踩一下
(0)
0%

Google提供的广告