老题常谈之跨域

  in   tech with  0  comment

“跨域”可以说是web开发中最经常遇到的问题之一,虽然比较简单,也容易比较解决。但是从这个问题却可以了解到相当多的知识点。这里也详细总结一下吧。

什么是跨域

由于安全的原因,浏览器做了很多方面的工作,其中之一就是同源策略。

同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

同源策略限制以下几种行为:

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

由此也就引入了一系列的跨域问题,需要注意的是:

跨域并非浏览器限制了发起跨站请求,而是跨站请求可以正常发起,但是返回结果被浏览器拦截了。最好的例子是 CSRF 跨站攻击原理,请求是发送到了后端服务器无论是否跨域!注意:有些浏览器不允许从HTTPS的域跨域访问HTTP,比如Chrome和Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例

为什么要有跨域

AJAX同源策略主要用来防止CSRF攻击。如果没有AJAX同源策略,相当危险,我们发起的每一次HTTP请求都会带上请求地址对应的cookie,那么可以做如下攻击:

1.用户登录了自己的银行页面 http://mybank.com,http://mybank.com向用户的cookie中添加用户标识。
2.用户浏览了恶意页面 http://evil.com。执行了页面中的恶意AJAX请求代码。
3.http://evil.com向http://mybank.com发起AJAX HTTP请求,请求会默认把http://mybank.com对应cookie也同时发送过去。
4.银行页面从发送的cookie中提取用户标识,验证用户无误,response中返回请求数据。此时数据就泄露了。
5.而且由于Ajax在后台执行,用户无法感知这一过程。

DOM同源策略也一样,如果iframe之间可以跨域访问,可以这样攻击:
1.做一个假网站,里面用iframe嵌套一个银行网站 http://mybank.com。
2.把iframe宽高啥的调整到页面全部,这样用户进来除了域名,别的部分和银行的网站没有任何差别。
3.这时如果用户输入账号密码,我们的主网站可以跨域访问到http://mybank.com的dom节点,就可以拿到用户的输入了,那么就完成了一次攻击。所以说有了跨域跨域限制之后,我们才能更安全的上网了。

跨域解决方案

在实际开发中,因为业务问题,我们的应用可能会分布在不同的域名或端口上,就会遇到同源策略的限制,解决这个问题有以下几种方案。

jsonp

原理

虽然名字叫做jsonp,实际上它的原理和json并没有关系。主要是利用标签的src属性没有受同源策略限制来获取来自其他域名的数据。我们通常使用 img / script 标签。最常用的就是 script 标签。因为js是自已执行其他域的函数的,而通过给函数传递参数,就可以达到跨域的目的。

使用方式

首先我们首先定义一个函数。

function callback(data){ console.log(data); }

然后引入一个其他域的js文件如:https://static.lscho.com/run_callback.js,在这个文件中调用 callback函数,并传入参数,我们就得到了来自其他域的数据。

callback({title:'jsonp演示'});

以上代码已插入本文页面中,打开控制台即可看到数据

利用这一点,我们可以给这个函数传入任意参数。所以我们后台就可以返回 callback(+任意数据+)来达到跨域目的。

因为jquery对jsonp的包装,如下。

$.ajax({ method: 'jsonp', url: 'http://example2.com', success: function(data) { console.log(data) } })

导致很多人认为jsonp是ajax相关的东西,其实并不是。Ajax 是利用 XMLHTTPRequest 来请求数据的,会受到同源限制。只是jquery为了api的一致性,对上述过程进行了包装。

jsonp也存在很多缺点,比如只支持get方式的请求,无法双向传输数据等。

window.name

原理

window对象中name参数是可以在多窗口(标签)内共享的,利用这一点,我们可以传输数据。结合 iframe 就可以有更为强大的功能。比如打开一个iframe并设置地址外域,在外域名设置window.name,再跳转回本域,此时还能获取到外域设置的window.name,此时就达到了跨域的目的。传输的数据,大小一般为2M,IE和firefox下可以大至32M左右,数据格式可以自定义。

使用方式

//a.html <script type="text/javascript"> var state = 0, iframe = document.createElement('iframe'), loadfn = function() { if (state === 1) { var data = iframe.contentWindow.name; // 读取数据 alert(data); //弹出'I was there!' } else if (state === 0) { state = 1; iframe.contentWindow.location = "http://a.com/proxy.html"; // 设置的代理文件 } }; iframe.src = 'http://b.com/b.html'; if (iframe.attachEvent) { iframe.attachEvent('onload', loadfn); } else { iframe.onload = loadfn; } document.body.appendChild(iframe); </script> //proxy.html //空文件放在a.com下起中转作用即可 //b.html <script type="text/javascript"> window.name = 'I was there!'; // 这里是要传输的数据,大小一般为2M,IE和firefox下可以大至32M左右,数据格式可以自定义,如json、字符串 </script>

缺点是比较麻烦,而且需要创建iframe,容易被xx浏览器拦截。

document.domain

原理

原理和window.name相似,在不同的子域 + iframe交互的时候,获取到另外一个 iframe 的 window对象是没有问题的,但是获取到的这个window的方法和属性大多数都是不能使用的。这种现象可以借助document.domain 来解决。

使用方式

//a.com <iframe id='i' src="b.com" onload="do()"></iframe> <script> document.domain = 'a.com'; document.getElementById("i").contentWindow; </script> //b.com <script> document.domain = 'a.com'; </script>

这样,就可以解决问题了。值得注意的是:document.domain 的设置是有限制的,只能设置为页面本身或者更高一级的域名。

使用比较方便,但是如果一个网站被攻击之后另外一个网站很可能会引起安全漏洞。

location.hash

原理

这种方法可以把数据的变化显示在 url 的 hash 里面。但是由于 chrome 和 IE 不允许修改parent.location.hash 的值,所以需要再加一层。

使用方式

例:a.html 和 b.html 进行数据交换。

//a.html function startRequest(){ var ifr = document.createElement('iframe'); ifr.style.display = 'none'; ifr.src = 'http://2.com/b.html#paramdo'; document.body.appendChild(ifr); } function checkHash() { try { var data = location.hash ? location.hash.substring(1) : ''; if (console.log) { console.log('Now the data is '+data); } } catch(e) {}; } setInterval(checkHash, 2000); //b.html //模拟一个简单的参数处理操作 switch(location.hash){ case '#paramdo': callBack(); break; case '#paramset': //do something…… break; } function callBack(){ try { parent.location.hash = 'somedata'; } catch (e) { // ie、chrome的安全机制无法修改parent.location.hash, // 所以要利用一个中间域下的代理iframe var ifrproxy = document.createElement('iframe'); ifrproxy.style.display = 'none'; ifrproxy.src = 'http://3.com/c.html#somedata'; // 注意该文件在"a.com"域下 document.body.appendChild(ifrproxy); } } //c.html //因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值 parent.parent.location.hash = self.location.hash.substring(1);

这样,利用中间的 c 层就可以用 hash 达到 a 与 b 的交互了。

window.postMessage()

原理

这个方法是 HTML5 的一个新特性,可以用来向其他所有的window对象发送消息。需要注意的是我们必须要保证所有的脚本执行完才发送MessageEvent,如果在函数执行的过程中调用了他,就会让后面的函数超时无法执行。

CORS

这个是今天介绍的重点了,这个是目前几乎最完美的解决方案。也是使用人数最多的方案。

原理

CORS 的全称是 Cross-Origin Resource Sharing,即跨域资源共享。他的原理就是使用自定义的 HTTP 头部,让服务器与浏览器进行沟通,主要是通过设置响应头的 Access-Control-Allow-Origin 来达到目的的。这样,XMLHttpRequest 就能跨域了。是一个W3C标准。

使用方式

设置响应头

字段名 是否可选 说明
Access-Control-Allow-Origin 必选 表示服务端允许的请求源,*标识任何外域,多个源 , 分隔
Access-Control-Allow-Credentials 可选 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true(同时ajax请求中withCredentials要设置位true),即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
Access-Control-Expose-Headers 可选 调用getResponseHeader()方法时候,能从header中获取的参数

可能我们会想到,同源策略是浏览器在限制,我们设置响应头有什么用?这时候浏览器还没发送请求呢。浏览器为了实现这个标准,将 CORS 请求分为了简单请求(simple request)和非简单请求(not-so-simple request)。简单请求就直接进行请求,而非简单请求,就会先发送一个 options 类型的嗅探请求,得到回应并检验通过之后才会进行后续的正常请求。

简单请求

(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

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。都以Access-Control-开头。

我们使用axios,来模拟一个请求。在这里使用 cnodejs 提供的接口服务。感谢。

axios.get('https://cnodejs.org/api/v1/topics');

点击执行

如果打开控制台我们会发现,该请求,虽然跨域,但是却直接请求成功了。就是因为该请求属于简单请求,看一下请求头和响应头。

20180505213620.png

非简单请求

凡是不同时满足简单请求条件,就属于非简单请求。对于非简单请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。类型为 options。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

我们模拟一个请求

axios.put('https://cnodejs.org/api/v1/topics');

点击执行

20180505214831.png

可以看到,有两次请求,第一次请求为 optios 类型,主要是为了检测该请求是否被允许,如果允许,才会继续进行下面的请求。

axios.put('https://baidu.com');

点击执行

我们再来执行一下这个代码,会发现,只有一个 options 类型的请求,并且控制台抛出异常。原因就是服务端没有允许发起跨域请求。浏览器通过这个 options 类型的请求,得到响应头,并抛出异常。

缺点,古老的浏览器没有实现该标准,所以不支持。由于复杂请求嗅探机制,会让服务器压力增大。

对比

对比以上,我们会发现 CORS 不仅使用方便,支持所有类型请求,具有权限控制,而且浏览器原生支持,我们可以轻易的处理请求异常。利于排查错误。所以我们大多数情况下会首选该方式。在低版本浏览器可以使用jsonp配合其他方式来兼容。

Responses