10 分钟理解跨域请求

10 分钟理解跨域请求_第1张图片
from @unsplash

互联网上的各个节点之间本来都是连通的,但是有些节点,比如我们的个人电脑连接另外一些节点(比如服务器)的时候,总是通过浏览器。这样,浏览器作为一个中间人,就有机会管理一些连接,就好像高速路上的收费站检查进出的车辆。

这个类比还有一个可以借鉴的地方:就像收费站会根据一些章程文件来检查进出车辆,浏览器也根据一些技术约定来管理进出的连接。这些技术约定由 W3C,WHATWG 等组织制定。

我们今天要谈的跨域请求,Cross Origin Resource Sharing ( CORS ),就是浏览器执行的一种技术约定,一种 Protocol。

那么具体来说,到底什么是“跨域请求”呢?

首先,“域”(Origin)是指浏览器地址栏中显示的主机地址。“跨域”,就是网页程序想要请求的地址和地址栏中的“域”不同。合起来就是,一个网页程序想要去别人家的地址[1]索要资源。

这个概念基本上只在涉及浏览器的网页应用中会出现。在浏览器之外,“域”的概念(浏览器地址栏中显示的主机地址)都没有了,更谈不上跨域了。但这个概念也是重要的,因为目前主流的浏览器,不论是电脑上的,还是手机上的,都支持这个约定;也就是说,不论是哪家的浏览器,只要在他们的地盘活动,这个约定是必须要考虑的。

如果违反这个 Protocol 中的约定,会有什么后果么?

  1. 有一些 request 会收不到 response,因为 response 被浏览器拦截了,内容不告诉你
  2. 另外一些请求会根本发不出去,因为浏览器不允许发出那样的 request。

接下来,我们就来讨论,哪些情况会导致收不到 response,哪些情况会导致 request 失败。

Simple Requests 如何跨域请求

我们定义一种 request 叫 simple request,当且仅当一个 request 满足下面全部条件的时候:

  1. request method 只能是 GET,HEAD,POST
  2. 只能有浏览器默认添加的 headers,以及一些 CORS 协议中默许的 headers 比如 Accept 等,更多被允许的 headers,可以看这里
  3. Content-Type这个header的value只能是特定的3个
  4. 在 js 代码中不要使用特定的方法,可参见文档

一旦一个 request 是 simple request,那么,尽管这个请求是跨域的,它也会被浏览器直接放行。但是,在 response 返回的时候,浏览器并不会把 response 直接交给你,而是去检查这个 response 的 headers 中有没有 Access-Control-Allow-Origin,以及这个 header 的 value 包含 request 发出的地址(也就是“域”)。

如果两个条件都满足, response 会被返回给发出请求的程序;如果没有这个 header 或者 value 不对, response 就会被拦截下来,因为在浏览器看来,这个 response 不属于你(因为服务器没有明确允许你这个“域”来请求它)。如果你使用的是 chrome 浏览器,在 response 被拦截下来的时候,console 中会显示一个类似于下面的错误信息:

10 分钟理解跨域请求_第2张图片
repeat this in your console

尽管发出request的程序无法得到 response,但是这个请求实际上是被发出了的,而且服务器也会完整的处理这个request。可以想见,如果被请求的服务器支持被跨域请求,那么它一定会想办法在 response 中加上Access-Controll-Allow-Origin这个 header,并且附上合适的值。什么是合适的值呢?在 Request 的headers中会有一个 Origin,只要Access-Controll-Allow-Origin包含这个 Origin 就可以了(如果是 wildcard *,那么就等于包含所有的 Origin)。

Preflighted Requests 如何跨域请求

进行跨域的请求只会被分为两类,第一类就是 Simple Requests,其他的都是 Preflighted Requests。所以,只要一个请求,不满足上面一小节中对 simple requests 的要求,就是 preflighted requests 了。也就是说,一个请求只要满足下面几个条件中的任意一个就可以被划归此类了:

  1. 不是 GET,HEAD,POST 请求;比如是 PUT 请求
  2. 包含一些非 CORS 协议默许的 headers,比如AuthorizationX-Request-With 或者一些自定义的headers。
  3. Content-Type 不是默许的那 3 种
  4. 在 js 代码中使用了特定的方法,可参见文档

看完了 preflighted requests 的入选条件,我们再来从字面和行为上理解一下这种跨域请求。preflight 的中文意思是起飞前的,而 preflighted 的意译可能是:被在起飞前搞了一番的(有点蹩脚,哈哈)。这种跨域请求的实际行为也确实包含 preflight (起飞前)的部分:在一个请求被发出之前,浏览器会先发一个 OPTIONS 请求到目标“域”的服务器上,这个提前发出的请求,被称为 preflight request。

讲完了 preflight request 这个最重要的概念,我们可以比较方便的梳理这种跨域请求的流程:

  1. 浏览器发送 preflight request(那个 OPTIONS 请求[2]
  2. 浏览器收到 preflight response(也就是刚刚那个 request 的返回)
  3. 浏览器根据 preflight response 中的 Access-Control-Allow-Origin, Access-Control-Allow-Headers以及其他Access-Control-\*类的headers 中的 value 来判断网页程序真正要发出的 request 是否符合要求
  4. 如果这个 request 符合要求,request 被发出,网页程序可以收到正常的 response(如果不出网络通讯上的意外);如果这个 request 被判定为不符合要求,这个 request 干脆就不会被发出。

有意思的是,以上这些步骤都是同步的,preflight request 和 真正的request 是有先后顺序的。不知道这在浏览器中是不是用 javascript 这种以异步为特点的语言实现的。

具体到这些 Access-Control-\* headers 怎么用于校验真正的 request 是否可以跨域:

  1. 可以根据 headers 的字面意思,已经可以理解大部分 headers 在控制什么
  2. 查看文档,看看 headers 的解释。

但是,还是有2个 headers 是比较难理解的(至少我看第一遍的时候没看懂,又去查别的资料),比如Access-Control-Expose-Headers,这个 header 是在控制 js 代码能访问哪些 headers:在跨域请求时,js 能访问的 response headers 只有默认的 6 个,其他的 headers,尽管浏览器已经收到,但并不会暴露给 js 程序,除非你在Access-Control-Expose-Headers中写明其他允许 js 代码访问的 headers。

还有另外一个 header,我们接下来讨论。

有身份的请求需要 Access-Control-Allow-Credentials

大家可能都知道,http 通讯是无状态的(stateless)。这不像是打电话,一旦建立连接,连接就不会来回断开更换,连接中两方的身份也就是确定的,不用重新确定身份。但客户端和服务器的http通讯并不强调建立一个长久的连接,两方在交换信息的时候,只要不表明身份,你都不知道同一个客户端之前和你有过通讯。然而,很多时候服务器需要追踪客户端上的信息,比如一个用户登录了,在浏览器上就要一直保持这个用户的身份。

让 http 请求带上一些身份信息并不是什么难事,通过在请求中加上一些用户信息就可以了,但如果请求是跨域的,CORS 协议中对于 3 种默认的表明身份(credential)的方式做了限制:

只有服务器明确表示允许,客户端才能这样表明身份

其中,服务器表明允许的方式就是通过在 response 中把Access-Control-Allow-Credentials 这个 header设为 true。另外,3 种默认的表明身份的方式是:

  1. Cookie: 在 request 的 header 中 Cookie 这一项
  2. Authorization: 在 request 的 header 中 Authorization 这一项
  3. 使用了 TLS 证书(还不太清楚是什么)

如果你的 request 中有上面 3 种东西中的至少一种,但是 server 返回的 response 中却没有Access-Control-Allow-Credentials: true,那么,根据这个 request 的类型(simple request 或者 preflighted request),浏览器会扣押 response 或者阻止真正的request发出。

最后,是关于一点 js 代码的小介绍。根据文档的说明,在进行跨域请求时,浏览器默认不会带上 cookie(这个 cookie 是针对目标域的 cookie,而不是原来“域”的 cookie),但是如果在构建 xhr 对象时,把XMLHttpRequest.withCredentials这个属性设为 true,浏览器会自动帮你带上目标域的cookie:

xhr.withCredentials = true;

后话

以上,simple requests, preflighted request 以及有身份的请求应该就是跨域请求的常见情况。

但有时候,跨域的请求竟然还可能被转发(收到了 301 ?),我们以后再讨论。


微信公众号:刘思宁


  1. 只要是浏览器以为的别人家,就是别人家的地址了 ↩

  2. 相比于 options 请求在一般的网页应用中较低的出现频率,options 在 cors 中出现的频率是很高的,所以甚至可以作为 preflight request 的一个特点 ↩

你可能感兴趣的:(10 分钟理解跨域请求)