当我们谈跨域的时候,我们在谈什么

561 阅读6分钟

跨域,即跨域资源共享——Cross-origin resource sharing,这是一个 w3c 标准,这个标准允许浏览器向非同源服务器发出 XMLHttpRequest 请求。

什么是非同源服务器呢?同源的定义是:两个 url 的 protocol(http/https)、port(省略的 80 端口/其它数字端口)、host(a.website.com/b.website.com/c.com) 都相同。protocol、port、host 三者有一个不同,就是非同源。

浏览器本身不允许跨域资源请求,是为了保证每个网站的用户信息——通常在 cookie 中保存着——不被其它网站窃取,目前,除了 cookie ,还有 localStorage、IndexDB 也不能跨域读取, dom 不能跨域获取、ajax 请求不能跨域发送,如此严格的政策之下,信息安全才能得以保证。

但在实际工作中,我们往往采用前后端分离的模式进行开发,跨域请求是难免的事情,开发模式下,浏览器的同源策略带来了很多不方便。

我们有哪些方案可以解决因同源策略引起的问题呢?

设置 document.domain

我们通常使用 local.myhostname 的方式进行开发,这种情况下,一级域名和线上一致,但二级域名不一致,若想使本地请求能够携带线上环境的 cookie ,怎么办呢?

可以通过

document.domain = 'myhostname' 

的方式,把当前域设置为当前域的父域了,那么后续的同源检查中,这个设置将告诉浏览器,当前页面希望被允许读取一级域名的 cookie。

现在,域名检查通过了,但通常我们开发环境下还会使用一个端口号,例如:8080、3001 ,等等,端口号是由浏览器另行检查的,设置了 document.domain 之后,端口号会被重写为 null ,而线上域名不是 null 。

因此通常我们使用 document.domain 赋值的方式允许跨域时,还需要在父域中同时设置

document.domain = 'myhostname'

,这样才能保证 host、port 的一致。

这种方法只适用于 cookie 读取和 iframe 窗口。

更通用的 cookie 共享方式是,服务器设置 cookie 的时候,指定 cookie 的所属域名为 .myhostname ,如:

Set-Cookie: key=value; domain=.example.com; ...

postMessage

为了解决 iframe 跨域情境下消息传递, html5 引入了一个 api —— 跨文档通信 API (Cross-document messaging)。

在此之前,人们可能使用 window.name 的方式,在非同源页面中传递消息,这是一种很 hack 的方式。

有了新的 API 之后,我们可以在一个页面中使用:

window.postMessage('info', target) // 父窗口向 iframe
window.opener.postMessage('info', target) // 子窗口向父窗口

来发送消息,在 target 页面中使用:

window.addEventListener('message', function(event) {
    // 检查 origin、source 可以验证发件人的身份,避免接收到其他来源的不安全的消息
    console.log(event.data, event.origin, event.source);
},false);

监听消息。

有了这个 API ,我们不仅可以给 iframe 传递简单的信息,也能将复杂的消息对象传递出去,这样一来,localStorage 信息也可以共享了。

CORS

ajax 请求只能发送给同源网址。我们可以采用代理的方式绕过同源策略,也可以使用 CORS 的方法来避开同源限制。

这里的 CORS 是一种机制,通过使用额外的 http 头部信息来告诉浏览器,某些 http 请求可以访问跨域资源。

整个 CORS 通信过程,都是浏览器自动完成,用户不会有感知。浏览器一旦发现 ajax 请求跨域,就会自动添加一些附加的头部信息,有时还会多出一次附加的请求。

非简单请求

比如,日常开发中我们经常能看到一种 OPTIONS 类型的 http 方法,这是因为在 CORS 中,面对非简单请求时,浏览器会首先使用 OPTIONS 方法发起一个预检请求,以检测实际请求是否可以被服务器接受。预检请求头部信息通常有:

OPTIONS /resources/post-here/ HTTP/1.1 
Origin: http://foo.example 
Access-Control-Request-Method: POST 
Access-Control-Request-Headers: Content-Type

接到请求后会根据请求头部信息判断,是否接受接下来的实际请求,如果接受,对应地返回:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://foo.example 
Access-Control-Allow-Methods: POST, GET, OPTIONS 
Access-Control-Allow-Headers: Content-Type 

如果拒绝,浏览器会返回一个正常的 http 响应,但不会包含任何 CORS 相关的头部信息字段。这时浏览器会打印出红色的跨域错误提示。

请注意,CORS 的实现需要浏览器和服务器同时支持,但关键是服务器。

上面提到了非简单请求。浏览器会将请求分成两类,简单请求和非简单请求,我们只要能区分简单请求,就能认识非简单请求。

简单请求

只要同时满足以下两个条件,就是简单请求:

  • 请求方法是 HEAD、GET、POST 之一

  • 头部信息不超出以下范围:

     Accept
     Accept-Language
     Content-Language
     Last-Event-ID
     Content-Type:只限于 application/x-www-form-urlencoded、multipart/form-data、text/plain
    

对跨域的简单请求,浏览器不会发送 OPTIONS 预检请求,而是直接在头部信息中增加 Origin 字段。

如果 Origin 指定的域名被服务器允许,返回的响应会多出几个字段:

Access-Control-Allow-Origin: somehost
Access-Control-Allow-Credentials: true
Content-Type: text/html; charset=utf-8

CORS 请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须 在Access-Control-Expose-Headers 里面指定:

Access-Control-Expose-Headers: X-xxxx

Cookie 携带

请注意,CORS 请求默认不发送 Cookie ,如果要发送 Cookie ,一方面服务器需要指定:

Access-Control-Allow-Credentials: true

另一方面,ajax 请求中,比如 fetch 的 header 中需要设置:

withCredentials: true

JSONP

JSONP 是一种简单适用的方式,浏览器兼容性很好,基本思想是,网页里添加一个 script 标签,向服务器请求 JSON 数据,服务器收到请求后,会把数据放在一个指定名字的回调函数里传回来。

可以这么做的原因是,JS 资源的加载不受同源策略限制。

除了 Js 资源,还有以下资源类型也不受同源策略限制:

<link rel="stylesheet" href="...">
通过 <img> 展示的图片
通过 <video> 和 <audio> 播放的多媒体资源
通过 <object>、 <embed> 和 <applet> 嵌入的插件
通过 @font-face 引入的字体。
通过 <iframe> 载入的任何资源。

一般使用一个随机的字符串来定义回调函数名称。

例如:

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

window.onload = function () {
  addScript('http://hostname/api?callback=myCallbackFunc')
}

function myCallbackFunc(data) {
  console.log(data)
}

服务器收到请求后,会将数据放在回调函数的参数位置返回:

myCallbackFunc({
  ...some props
});

由于 script 元素请求的脚本,直接作为代码运行,这时,只要浏览器定义了 myCallbackFunc 函数,函数就会立即调用。作为参数的 JSON 数据就是一个 js 对象,不需要再次解析。

但是 JSONP 只能用于 GET 方法的 http 请求,与 CORS 相比限制比较多。

总结

以上是常用的一些跨域方案,随着开发方式的演进,一些方案已经很少使用到,但了解它们有助于我们理解一些深层原理。