前端跨域解决方案

879 阅读7分钟

前言

作为一个前端开发,跨域问题是我们开发过程中经常遇到,也是面试必考题之一。所以本文就来总结一下最常用的跨域解决方案,以供参考。

同源策略

浏览器中有一个很重要的安全性限制,称为同源策略(Same-Origin Policy)。同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。所谓的同源,就是两个页面的协议,端口和主机都相同。

源是会被继承的,当在页面中用 about:blank 或 javascript: URL 执行脚本时,会继承打开改文档的URL的源,因为这些类型的 URLs 没有明确包含有关原始服务器的信息。

源也是可以被修改的,JS中可以将 document.domain 的值设置为其当前域或其当前域的父域。注意,如果要使用 document.domain 来允许子域访问父域时,需要将子域和父域的 document.domain 设置成相同的值,否则可能会导致权限错误。这也是跨域问题的解决方案之一。

同源策略控制着不同源之间的交互,但不是所有的跨域操作都是会被阻止的:

  1. 通常允许跨域写操作,例如a标签的链接跳转,重定向等;
  2. 允许跨域资源的迁入,例如script、img、video、iframe等标签;
  3. 不允许跨域的读操作,但是可以通过内嵌资源或者设置同源策略来巧妙的进行读取操作;
  4. 不允许跨域的数据存储访问,例如localStorage和IndexedDB;
  5. 允许访问本域和任何父域的cookie,不过这里涉及到一个公共后缀(public suffix)的问题,就不展开说明了。

下面就介绍几种常见的跨域操作:

JSONP

最常见的跨域方法就是jsonp跨域了,用法也很简单,它的实现原理也很简单:

  1. 先是利用script标签来实现跨域请求资源;
  2. 服务端将前端传的callback作为方法名,json数据作为参数返回,实现数据的跨域传输。

不足之处:

  1. 只能支持GET请求,script、img等标签引入的资源都是GET请求,所以jsonp请求就一定是GET请求;
  2. 回调方法是一个全局方法,我们都知道要尽量减少全局方法;
  3. 会有一些诸如XSS、CSRF攻击等的安全隐患。

跨源资源共享(CORS)

跨源资源共享(CORS)是一种机制,它使用额外的HTTP标头告诉浏览器让在一个源(域)上运行的Web应用程序有权从不同来源的服务器访问所选资源。CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

通常使用CORS时,异步请求会被分为简单请求非简单请求,非简单请求的区别是会先用OPTIONS方法发一次预检请求,从而获取服务端是否允许该跨域请求。

简单请求

同时满足以下两个条件的请求为简单请求:

  1. 请求的方法是HEAD/GET/POST之一;
  2. HTTP头信息不超出以下几个字段:

Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type,其中 Content-Type 只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

在发送简单请求的时候,浏览器会在头信息中加一个Origin字段,前端开发不需要任何操作。但是服务端需要设置Access-Control-Allow-Origin属性来支持当前请求的域名,可以指定为 * 来允许所有域名。

当然,这时候发送的请求是最简单的情况。如果你要带上Cookie的话,需要在服务端设置 Access-Control-Allow-Credentials 为true,表示服务端允许请求中带上Cookie。同时前端需要设置withCredentials为true,否则浏览器也是不会发送Cookie的。例如vue框架如下设置:

// axios设置:
axios.defaults.withCredentials = true
// vue-resource设置:
Vue.http.options.credentials = true

非简单请求

只要是不同时满足简单请求的两个条件的,就是非简单请求。例如PUT或DELETE类型的,或者Content-Type是application/json的情况。

非简单请求在正式通讯前,浏览器会自动发送一个预检请求,即我们能在控制台里看到的options请求。

预检请求的头信息里有3个关键字段需要注意:

  1. Origin字段,表示请求的域名;
  2. Access-Control-Request-Method字段,表示正式请求的类型,例如GET、PUT等;
  3. Access-Control-Request-Headers字段,是一个用逗号隔开的字符串,表示正式请求中会额外发送的头信息字段。

浏览器需要询问服务器对上述字段的审核通过后,得到肯定的结果后才会发起正式的请求。

如果请求要带上Cookie,则必须在 XHR 设定 withCredentials 或是 fetch 的选项中设置 { credentials: 'include' },且需要设置HTTP头部Access-Control-Allow-Credentials: true。

我们可以思考一个问题,如果这种非简单请求需要多次请求,那么是不是每次都得发送两遍请求呢,这看起来会很愚蠢。答案是否定的,OPTIONS的请求结果是可以被缓存的。响应头信息中的Access-Control-Max-Age就是表示预检请求的结果被缓存最长时间,单位是秒。如果值为-1表示禁用缓存。当然我们在平时请求的时候应该尽量避免预检请求,除了一些特殊的场景,其实我们平时发送的axios或者fetch请求都是可以通过配置来避免预检请求的。

CORS策略的优缺点:

优点:

  1. CORS支持所有类型的HTTP请求。
  2. 使用CORS,开发者可以使用普通的XMLHttpRequest发起请求和获得数据,比起JSONP有更好的错误处理。

缺点:

  1. 兼容性方面相对差一点,ie10或以上才支持

postMessage

postMessage是一种页面之间的通讯方式,它可以在页面和它产生的弹窗,iframe之间实现页面之间的跨域通讯。 调用语法为:

otherWindow.postMessage(message, targetOrigin, [transfer]);

otherWindow是其他窗口的引用。例如创建的iframe对象的contentWindow属性、window.open返回的对象或者是命名过或数值索引的window.frames。 message是需要发送的数据。 targetOrigin是通过窗口的origin属性来指定哪些窗口能接收到消息事件,可以设置为"*"表示无限制,或者一个url。 transfer是一个可选值,是一串和 message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

监听postMessage分发数据的方法如下:

window.addEventListener("message", receiveMessage, false);

receiveMessage接收的参数一个参数,我们暂且命名为event。

  • event.data是接收的数据
  • event.origin是发送消息窗口的origin
  • event.source是发送消息来源的window对象

在使用postMessage的时候需要特别注意安全问题,当使用postMessage将数据发送到其他窗口时,始终指定精确的目标origin,而不是*。恶意网站可以拦截使用postMessage发送的数据。接收数据时,需要使用origin和source属性验证发件人的身份。

document.domain跨域

当父子页面的域不同的时候虽然可以获取到window对象,但是无法获取到对应的方法和属性。但是文章开头说到的 document.domain 可以把两个页面设置为相同的域,这样就可以达到我们的目的了。 例如父页面(www.domain.cn/a.html)

<script type="text/javascript">
function load(){
    var iframe = document.getElementById('ifame');
    var win = iframe.contentWindow; // 这里就是子页面的window对象
}
</script>
<iframe id = "iframe" src="http://domain.cn/b.html" onload = "load()"></iframe>

子页面(domain.cn/b.html)

<script type="text/javascript">
    document.domain = 'domain.cn';
</script>