原文连接:http://www.devsai.com/2016/11/24/talk-CORS/javascript
1995年,同源政策由 Netscape 公司引入浏览器。为了防止某些文档或脚本加载别的域下的未知内容,防止形成泄露隐私,破坏系统等行为发生。html
同源策略作了两种限制:前端
不能经过ajax的方法或其余脚本中的请求去访问不一样源中的文档。html5
浏览器中不一样域的框架之间是不能进行js的交互操做的。java
如今全部的可支持javascript的浏览器都会使用这个策略。web
URL的三部分彻底相同时咱们就能够称其为同源,这三部分是: 协议
,域名(主机名)
和端口
都相同。ajax
当涉及到同源策略时,Internet Explorer有两个主要的例外json
授信范围(Trust Zones):两个相互之间高度互信的域名,如公司域名(corporate domains),不遵照同源策略的限制。 端口:IE未将端口号加入到同源策略的组成部分之中,所以 http://company.com:81/index.html 和http://company.com/index.html 属于同源而且不受任何限制。
虽然同源策略颇有必要,但有不少时候咱们仍是须要去请求其余域的数据,如:调用不一样业务的数据,而不一样业务已子域区分;又或者是第三方公用的数据接口等等canvas
因为各类缘由,咱们须要经过各类方式来请求到不一样域下的资源。windows
jsonp是经过能够发出跨域请求的script标签,使javascript可以得到跨域请求的数据,并调用数据。
先看个例子:
文件index.js :
alert(123);
页面index.html:
... <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" : "devsai",like:"everything"}}
。获得数据后在ajax中调用
showUserInfo(data)
来渲染页面,data
就是接口数据。
若是如今用script标签来请求数据,那么一样能够得到数据,执行返回到的内容,因是json
格式的数据,并不会报错,但也并无卵用。得到接口的数据确定是想作些什么的。
再想一想,正常ajax请求后的js执行内容showUserInfo(data)
,拿到数据后,调用了showUserInfo
函数。
那么,用script标签来请求数据时,返回的内容直接是showUserInfo(data)
不就好了,但服务端又不知道咱们到底要执行哪一个函数,即便事先约定了,但后面因某些事要改,那还得告诉服务端,太麻烦了。
若是知道要执行什么函数就行了。
固然,这是能够的,改造下接口,以参数的形式把函数名传给服务端。
<script src="/jsonp/getUserInfo/001?jsonp_fn=showUserInfo"></script>
Response返回的内容一样须要改造
Response: showUserInfo({"data" : {"name" : "devsai",like:"everything"}})
这样,经过jsonp,去跨域请求接口数据就完成了。
须要注意的是函数名须要挂在window
下面,要否则会报函数名未定义。
例如在demo.devsai.com/index.html
页面里执行以下内容:
document.domain = 'devsai.com';
执行该语句后,能够成功经过devsai.com/index.html
的同源检测, 实现数据的通信,
固然document.domain
不能随意设置,只能设置成当前域,或设置成当前域的顶域。
document.domain经常被用于同站但不一样域的状况,例如:www.devsai.com
,下嵌入了iframe广告页面ad.devsai.com
,想要实现两页面的通信,就须要对两个页面都设置document.domain='devsai.com'
。
window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的全部的页面都是共享一个window.name的,每一个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的全部页面中的,并不会因新页面的载入而进行重置。
name只能是字符串。
页面a.html中:
<script> window.name = 'page name index.html'; setTimeout(function(){ window.location = 'http://localhost:8080/static/b.html'; }, 2000); </script>
页面b.html:
<script> alert(window.name); </script>
再来看看如何让a.html页面获取数据
用data.html做为请求数据地址:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Data</title> </head> <body> <script> window.name = '{ "name":"devsai","like" : "everything"}'; // 须要传入 a.html页面的数据,必须是字符串 </script> </body> </html>
a.html:
... <iframe src="http://localhost:8080/static/data.html" onload="getData();" frameborder="0" id="iframe_1"></iframe> <script> function getData(){ var iframe = document.getElementById('iframe_1'); //隐藏iframe iframe.setAttribute("width", "0"); iframe.setAttribute("height", "0"); iframe.setAttribute("border", "0"); iframe.setAttribute("style", "width: 0; height: 0; border: none;"); iframe.onload = function(){ console.log(iframe.contentWindow.name); var data = iframe.contentWindow.name; data = JSON.parse(data);//转成 JSON showUserInfo(data); } iframe.src = 'about:blank'; } function showUserInfo(data){ console.log(data); // .....do something } </script> ...
当访问http://127.0.0.1:8080/static/index.html
,便能得到来自不一样域下data.html
中的数据。
也能够作的更完善些,动态的生成iframe请求数据,用完即毁。
.... // 传入请求数据接口地址和回调函数 function requestData(url,successCB){ var body = document.getElementsByTagName('body')[0]; var iframe = document.createElement("iframe"); iframe.setAttribute("id", "getDataByWindowName"); iframe.setAttribute("width", "0"); iframe.setAttribute("height", "0"); iframe.setAttribute("border", "0"); iframe.setAttribute("style", "width: 0; height: 0; border: none;"); iframe.setAttribute("src", url); body.appendChild(iframe); setTimeout(function(){//防止iframe.src在没加载前就被替换 iframe.onload = function(){ var data = iframe.contentWindow.name; if(data){ data = JSON.parse(data);//转成 JSON successCB && successCB(data); } iframe.parentNode.removeChild(iframe); } iframe.src = 'about:blank'; }, 100); } //requestData("http://localhost:8080/static/data.html",showUserInfo); ...
这就是使用window.name来进行跨域。
window.postMessage方法是html5的新特性之一,
可使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不一样源。
经过window.postMessage容许浏览器windows, tabs, and iFrames之间跨域通信。
以前写过一篇关于window.postMessage
的,作了详细的说明+演示页面+演示代码,去看看
例如一个网站上有各类不一样的业务,不一样的业务有其对应的子域。
如:ad.devsai.com;upload.devsai.com;live.devsai.com
,分别对应广告业务,上传业务,直播业务。
想在www.devsai.com
中作交互,或得到数据,便会受跨域影响。
形成跨域的缘由是由于请求数据的源不一样,那只要请求的源同样,便没有跨域问题了。
这也是能够办到的,只须要web服务作下代理,或称之为地址映射。
拿Nginx举例,须要在web服务上作以下配置:
... lcaotion /ad { proxy_pass http://ad.devsai.com } location /upload { proxy_pass http://upload.devsai.com } location /live { proxy_pass http://live.devsai.com } ...
而后就能够在以www.devsai.com/ad/
的方式去调用广告业务。
当一个发起的请求地址与发起该请求自己所在的地址不在同源下时,称该请求发起了一个跨域的HTTP请求。
有些的跨域请求是被容许的<img>
,<script>
,<link>
图片,脚本,样式及其余资源 ,加载这些数据时即便不在同源下面也一样被容许,现在的网站一般也会去引用不在同源下的这些资源,如作CDN加速。
但也有些不被容许,正如你们所知,出于安全考虑,浏览器会“限制”脚本中发起的跨站请求,好比:XmlHttpRequest。
除了XmlHttpRequest外,还有如下几种跨域请求作了相应的安全限制。
好比:
1 前面说的iframe,经过设置src
能够发起跨域请求,但对请求到的内容进行操做就不被容许了。如执行iframe.contentWindow.name
就会报错。
2 <img>
标签上crossorigin
属性是一个CORS的配置属性,目的是为了容许第三方网站上的图片(即不在同源上的图片)可以在canvas中被使用。
若是没有配置改属性,又跨域请求了图片,当调用canvas中的toBlob()
, toDataURL()
, 或getImageData()
方法的时候,会报错。
var img = new Image, canvas = document.createElement("canvas"), ctx = canvas.getContext("2d"), src = "https://sf-sponsor.b0.upaiyun.com/45751d8fcd71e4a16c218e0daa265704.png"; // insert image url here img.crossOrigin = "Anonymous"; img.onload = function() { console.log(img); canvas.width = img.width; canvas.height = img.height; ctx.drawImage( img, 0, 0 ); localStorage.setItem( "savedImageData", canvas.toDataURL("image/png") ); } 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和Firefox,这些浏览器在请求还未发出的时候就会拦截请求。
解决这类跨域问题的方法就是*CORS*
,对于简单的请求来讲,前端这边都不须要作任何的编码就能实现跨域请求,
只须要服务端配置响应头"Access-Control-Allow-Origin:*"。
CORS是一个W3C标准,全称“跨域资源共享”(Cross-origin resource sharing)
跨源资源共享标准经过新增一系列 HTTP 头,让服务器能声明哪些来源能够经过浏览器访问该服务器上的资源。
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的有两种请求方式: 简单请求(Simple Request) 和 预请求(Prefilght Request)
只要同时知足如下三大条件,就属于简单请求。
a) 请求方式是如下几种方式之一
* GET * POST * HEAD
b) content-type必须是如下几种之一
* application/x-www-form-urlencoded * multipart/form-data * text/plain
c) 不会使用自定义请求头(相似于 X-Modified 这种)。
若是不知足简单请求的三大条件,会在发送正真的请求前,发送个请求方式为'OPTIONS'的请求,去服务端作检测,
1) 请求方式不是GET,POST,HEAD
那么须要在响应HEAD配置容许的请求方式,例如:Access-Control-Allow-Methods:PUT,DELETE
2) 使用自定义请求头,如x-devsai ,那么须要在服务端相应配置容许的自定义请求头:Access-Control-Request-Headers: x-devsai
一旦检测不经过,浏览器就会提示相应的报错,并不会发生真实的请求。
从上图可只IE11,如下的就不支持CORS了。但实际上再IE8,IE9,IE10中,能够用XDomainRequest对象代替XmlHttpReuqest,发送跨域请求。
var xdr = new XDomainRequest(); xdr.open("get", "http://www.devsai.com/xdr"); 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__
缺点: 也是兼容性问题
真正开发过程当中,需针对不一样状况,使用不一样的解决之法。