同源策略与JS跨域(JSONP , CORS)

本文按照政治问答题必备套路分为如下3个部分:javascript

  1. 为何要跨域?
  2. 跨域是什么?
  3. 如何实现跨域?

Section一、为何要跨域?

自古以来(1995年起),为了用户的信息安全,浏览器就引入了同源策略。那么同源策略是如何保证用户的信息安全的呢?html

栗子1:若是没有同源策略,你打开了你的银行帐户页面A,又打开了另外一个不相关的页面B,这时候若是B是恶意网站,B能够经过Javascript轻松访问和修改A页面中的内容。前端

栗子2:如今咱们普遍的使用cookie来维护用户的登陆状态,而若是没有同源策略,这些cookie信息就会泄露,其余网站就能够冒充这个登陆用户。html5

由此能够看出,同源策略确实是必不可少的,那么它会带来哪些限制呢?java

一、Cookie、LocalStorage和IndexDB没法读取。
二、DOM没法得到。
三、AJAX请求不能发送。segmentfault

有时候咱们须要突破上述限制,就须要用跨域的方法来解决。api

Section二、跨域是什么?

什么叫作不一样的域?好比:跨域

http://www.a.com:8000/a.js
协议(http)、域名(www.a.com)、端口(8000)三者中有一个不一样就叫不一样的域。浏览器

跨域就是不一样的域间相互访问时使用某些方法来突破上述限制。安全

【注意:协议或者端口的不一样,只能经过后台来解决。】

Section三、怎么跨域?

1、解决Section1中提到的一、2两点限制:

1.Cookie、LocalStorage和IndexDB没法读取。
2.DOM没法得到。

方法一、经过document.domain跨子域

【适用范围:(1)两个域只是子域不一样;(2)只适用于iframe窗口与父窗口之间互相获取cookie和DOM节点,不能突破LocalStorage和IndexDB的限制。】

当两个不一样的域只是子域不一样时,能够经过把document.domain设置为他们共同的父域来解决。

栗子:

A: http://www.example.com/a.html
B: http://example.com/b.html
当A、B想要获取对方的cookie或者DOM节点时,能够设置:

document.domain='example.com';

来解决。

这时A网页经过脚本设置:

document.cookie = "testA=hello";

B网页就能够拿到这个cookie:

var aCookie = document.cookie;

方法二、经过window.name跨域

【适用范围:(1)能够是两个彻底不一样源的域;(2)同一个窗口内:即同一个标签页内前后打开的窗口。】

pre-condition:

window.name属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的全部的页面都是共享一个window.name的,每一个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的全部页面中的。

基于这个思想,咱们能够在某个页面设置好 window.name 的值,而后在本标签页内跳转到另一个域下的页面。在这个页面中就能够获取到咱们刚刚设置的 window.name 了。

结合iframe还有更高级的用法:

父窗口先打开一个与本身不一样源的子窗口,在这个子窗口里设置:

window.name = data;

而后让子窗口跳转到一个与父窗口同域的网址:

location='http://www.parent.com/a.html';

这时,由于同域而且同一窗口window.name是不变的,因此父窗口能够获取到子窗口下的window.name。

var data = document.getElementById('myFrame').contentWindow.name;

优势:window.name容量很大,能够放置很是长的字符串;缺点:必须监听子窗口window.name属性的变化,影响网页性能。

方法三、使用HTML5的window.postMessage跨域

window.postMessage(message,targetOrigin) 方法是html5新引进的特性,可使用它来向其它的window对象发送消息,不管这个window对象是属于同源或不一样源,目前IE8+、FireFox、Chrome、Opera等浏览器都已经支持window.postMessage方法。

otherWindow.postMessage(message, targetOrigin);

otherWindow:接受消息页面的window的引用。能够是页面中iframe的contentWindow属性;window.open的返回值;经过name或下标从window.frames取到的值。
message:所要发送的数据,string类型。
targetOrigin:用于限制otherWindow,*表示不作限制。

栗子1:

在父页面中嵌入子页面,经过postMessage发送数据。
parent.com/index.html中的代码:

<iframe id="ifr" src="child.com/index.html"></iframe>
<script type="text/javascript">
window.onload = function() {
    var ifr = document.getElementById('ifr');
    var targetOrigin = 'http://child.com'; 
    // 若写成'http://child.com/c/proxy.html'效果同样
    // 若写成'http://c.com'就不会执行postMessage了
    ifr.contentWindow.postMessage('I was there!', targetOrigin);
};
</script>

在子页面中经过message事件监听父页面发送来的消息并显示。
child.com/index.html中的代码:

<script type="text/javascript">
window.addEventListener('message', function(event){
    // 经过origin属性判断消息来源地址
    if (event.origin == 'http://parent.com') {
        alert(event.data);    // 弹出"I was there!"
        alert(event.source);  
        // 对parent.com、index.html中window对象的引用
        // 但因为同源策略,这里event.source不能够访问window对象
    }
}, false);
</script>

栗子2:

假设在a.html里嵌套个

<iframe src="http://www.child.com/b.html" frameborder="0"></iframe>

在这两个页面里互相通讯

a.html

window.onload = function() {
    window.addEventListener("message", function(e) {
        alert(e.data);
    });

    window.frames[0].postMessage("b data", "http://www.child.com/b.html");
}

b.html

window.onload = function() {
    window.addEventListener("message", function(e) {
        alert(e.data);
    });
    window.parent.postMessage("a data", "http://www.parent.com/a.html");
}

这样打开a页面,首先监听到了b.html经过postMessage传来的消息,就先弹出 a data,而后a经过postMessage传递消息给子页面b.html,这时会弹出 b data。

2、解决第3点限制:

3)AJAX请求不能发送。

方法四、经过JSONP跨域

【适用范围:(1)能够是两个彻底不一样源的域;(2)只支持HTTP请求中的GET方式;(3)老式浏览器所有支持;(4)须要服务端支持】

JSONP(JSON with Padding)是资料格式JSON的一种使用模式,可让网页从别的网域要资料。

因为浏览器的同源策略,在网页端出现了这个“跨域”的问题,然而咱们发现,全部的 src 属性并无受到相关的限制,好比 img / script 等。

JSONP 的原理就要从 script 提及。script 能够引用其余域的脚本文件,好比这样:

a.html
...
<script>
    function callback(data) {
        console.log(data.url)
    }
</script>
<script src='b.js'></script>
...

b.js
callback({url: 'http://www.rccoder.net'})

这就相似于JSONP的原理了。

JSONP的基本思想是:先在网页上添加一个script标签,设置这个script标签的src属性用于向服务器请求JSON数据 ,须要注意的是,src属性的查询字符串必定要加一个callback参数,用来指定回调函数的名字 。而这个函数是在资源加载以前就已经在前端定义好的,这个函数接受一个参数并利用这个参数作一些事情。向服务器请求后,服务器会将JSON数据放在一个指定名字的回调函数里做为其参数传回来。这时,由于函数已经在前端定义好了,因此会直接调用。

一个栗子:

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

window.onload = function () {
    addScriptTag('http://example.com/ip?callback=foo');//请求服务器数据并规定回调函数为foo
}

function foo(data) {
    console.log('Your public IP address is: ' + data.ip);
};

向服务器example.com请求数据,这时服务器会先生成JSON数据,这里是{"ip": "8.8.8.8"},而后以JS语法的方式生成一个函数,函数名就是传递上来的callback参数的值,最后将数据放在函数的参数中返回:

foo({
    "ip": "8.8.8.8"
});

客户端解析script标签,执行返回的JS代码,调用函数。

方法五、经过CORS跨域

【适用范围:(1)能够是两个彻底不一样源的域;(2)支持全部类型的HTTP请求;(3)被绝大多数现代浏览器支持,老式浏览器不支持;(4)须要服务端支持】

对于前端开发者来讲,跨域的CORS通讯与同源的AJAX通讯没有差异,代码彻底同样。所以,实现CORS通讯的关键是服务器。只要服务器实现了CORS接口,就能够跨源通讯。

浏览器将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

凡是不一样时知足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不同的。

简单请求:

下面是一次跨源AJAX请求,浏览器发现它是简单请求,就会直接在头信息中加一个origin字段:

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

服务器收到这条请求,若是这个origin指定的源在许可范围内,那么服务器返回的头信息中会包含Access-Control-Allow-Origin字段,值与origin的值相同,以及其余几个相关字段:

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar

Access-Control-Allow-Origin: 该字段是必须的。要么与origin相同,要么为*
Access-Control-Allow-Credentials: 该字段可选。设为true表示服务器容许发送cookie
Access-Control-Expose-Headers: 该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。若是想拿到其余字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')能够返回FooBar字段的值。

想要发送cookie,这里还有两点须要额外注意:

1)开发者必须在AJAX请求中打开withCredentials属性。

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

不然即便服务器容许,客户端也不会发送。

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

非简单请求:

1.预检请求:

非简单请求会在正式通讯前加一次预检(preflight)请求。做用是浏览器先询问服务器当前网页所在域名是否在服务器的许可名单中,以及可使用哪些HTTP方法以及头信息字段。只有获得确定答复,浏览器才会发送XMLHttpRequest,不然报错。

一个栗子:

var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

HTTP请求方法为PUT,并发送一个自定义头信息"X-Custom-Header",浏览器发现这是一个非简单请求,就会自动发送一个预检请求,预检请求的HTTP头信息以下:

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

请求方法是OPTIONS,表示这个请求是用来询问的,头信息中的关键信息有3个:

(1)表示请求来自哪一个源

Origin: http://api.bob.com

(2)列出浏览器的CORS请求会用到哪些HTTP方法

Access-Control-Request-Method: PUT

(3)指定浏览器CORS请求会额外发送的头信息字段

Access-Control-Request-Headers: X-Custom-Header

2.预检请求的回应(有两种状况:A容许、B不容许)

A.服务器容许此次跨域请求

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

服务器返回中要注意的字段:

(1)服务器赞成的跨域请求源:

Access-Control-Allow-Origin: http://api.bob.com

(2)服务器支持的全部跨域请求的方法:

Access-Control-Allow-Methods: GET, POST, PUT

(3)代表服务器支持的全部头信息字段:

Access-Control-Allow-Headers: X-Custom-Header

(4)指定本次预检请求的有效期,单位为秒,即容许请求该条回应在有效期以前都不用再发送预检请求:

Access-Control-Max-Age: 1728000

B.服务器不容许此次跨域请求
即origin指定的源不在许可范围内,服务器会返回一个正常的HTTP回应。可是头信息中没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。可是要注意的是,这种HTTP回应的状态码颇有多是200,因此没法经过状态码识别这种错误。

3.正式请求
过了预检请求,非简单请求的正式请求就与简单请求同样了。

参考资料

  1. 《Javascript高级程序设计》 P582
  2. 《Javascript权威指南》 P668 22.3 跨域消息传递
  3. http://www.ruanyifeng.com/blo...
  4. http://www.ruanyifeng.com/blo...
  5. https://segmentfault.com/a/11...
  6. https://segmentfault.com/a/11...
  7. https://segmentfault.com/a/11...
  8. http://www.cnblogs.com/rainma...
相关文章
相关标签/搜索