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

11

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

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

Section1、为什么要跨域?

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

栗子1:如果没有同源策略,你打开了你的银行账户页面A,又打开了另一个不相关的页面B,这时候如果B是恶意网站,B可以通过Javascript轻松访问和修改A页面中的内容。

栗子2:现在我们广泛的使用cookie来维护用户的登录状态,而如果没有同源策略,这些cookie信息就会泄露,其他网站就可以冒充这个登录用户。

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

1、Cookie、LocalStorage和IndexDB无法读取。
2、DOM无法获得。
3、AJAX请求不能发送。

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

Section2、跨域是什么?

什么叫做不同的域?比如:

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

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

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

Section3、怎么跨域?

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

1.Cookie、LocalStorage和IndexDB无法读取。
2.DOM无法获得。

方法1、通过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;

方法2、通过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属性的变化,影响网页性能。

方法3、使用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。

二、解决第3点限制:

3)AJAX请求不能发送。

方法4、通过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代码,调用函数。

方法5、通过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...

你可能感兴趣的

geosmart · 2017年06月12日

细致好文

回复

火狼 · 2018年02月09日

这应该是我见过最全面,最详细的一篇关于跨域的分析,点赞

回复

0

如果加上web scokets跨域就更完善啦

火狼 · 2018年02月09日
载入中...