说起跨域,每个前端开发人员都不陌生,甚至想当场掉一把头发,跨域访问资源对于前端开发非常重要,尤其是在流行前后端分离的开发模式后,前后端可能被分别部署,数据大部分通过接口获取,但由于浏览器的同源策略限制,跨域又变得比较困难,为此,出现了很多跨域的解决方案,例如:后端代理、JSONP、postMessage 等等,但这些方法并不标准,甚至有些思路有点狂野,为了解决这个问题,W3C 制定了跨域资源共享(CORS)标准。

什么是 CORS

跨域资源共享(CORS:Cross-Origin Resource Sharing),通过一组新增的 HTTP 首部字段,允许服务器声明哪些域可以通过浏览器访问自身的资源。

那么,与 CORS 相关的 HTTP 首部字段有那些呢?

与 CORS 相关的请求报文字段:
Origin
Access-Control-Request-Method
Access-Control-Request-Headers
与 CORS 相关的响应报文字段:
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Credentials
Access-Control-Expose-Headers
Access-Control-Max-Age

以上就是 CORS 相关的 HTTP 首部字段,这里只是做一个简单的列表,下面将会结合 CORS 工作方式和实例进行详细的功能说明。

CORS 如何工作

在通信过程中,使用 OriginAccess-Control-Allow-Origin 就能完成最简单的访问控制,例如:

GET /api HTTP/1.1
Host: www.b.com

我们假设这个请求是从 http://www.a.com 发往 http://www.b.com 的,浏览器发现请求跨域,就会在请求报文中添加 Origin 字段,它的作用是用来表示请求来自于哪个域:

Origin: http://www.a.com

服务端接收此报文后,如果允许该域访问自身的资源,那么就通过响应报文的 Access-Control-Allow-Origin 字段进行回复:

Access-Control-Allow-Origin: http://www.a.com

注意:Access-Control-Allow-Origin 的值也可以设置为 “*”,表示该资源可以被任意外域访问。

浏览器接收到此响应报文后,知道了服务端允许跨域访问,于是解除同源策略限制,这样就完成了一次跨域请求。那么,实际通信的报文是这样的:

# 请求报文
GET /api HTTP/1.1
Host: www.b.com
Origin: http://www.a.com

# 响应报文
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://www.a.com

可以看到,这种方式是在原始报文中增加头部字段实现的,比较简单,所以叫它简单请求,但简单请求需要满足以下条件:

如果跨域请求不满足上述条件,那么浏览器在发出正式请求之前,会与服务端进行一次 HTTP 通信,这个过程就叫做预检(Preflight)。预检的作用是与服务端进行一次预先检查,判断服务端是否能够接收实际请求,避免产生未预期的错误。

例如下面这个请求:

POST /api HTTP/1.1
Host: www.b.com
Content-Type: application/json
X-NAME: X-123

{"x":1,"y":2}

可以看到,原始报文的 Content-Type 和 X-NAME(自定义的)都不符合简单请求的要求,于是开始执行预检,预检通过后再进行实际报文的通信:

# 预检过程开始 ---------------

# 预检请求报文
OPTIONS /api HTTP/1.1
Host: www.b.com
Origin: http://www.a.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, X-NAME

# 预检响应报文
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://www.a.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-NAME
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-ID, X-AGE
Access-Control-Max-Age: 86400

# 预检过程结束 ---------------

# 请求报文
POST /api HTTP/1.1
Host: www.b.com
Content-Type: application/json
X-NAME: X-123
Origin: http://www.a.com

{"x":1,"y":2}

# 响应报文
HTTP/1.1 200 OK
X-ID: 1212
X-AGE: 19
Access-Control-Allow-Origin: http://www.a.com

下面以此为例说明预检过程。

预检请求报文

预检请求是根据实际请求产生的,它必须使用 OPTIONS 方法,还包括几个相关头部字段:

Origin

和简单请求一样,预检请求同样需要使用 Origin 字段表示请求的源域。

Access-Control-Request-Method

用于表示实际请求将会使用的方法,服务端通过此字段判断能否接受实际请求使用的方法。在上面的例子中,原始报文的请求方法是 POST,所以其值为 POST。

Access-Control-Request-Headers

用于通知服务器实际请求中所携带的首部字段,这些字段通常是经过修改或自定义的。在上面的例子中,原始报文设置了 Content-Type,并且自定义了一个名为 X-NAME 字段,它们都被预检请求告知给服务端,由服务端判断其能否被允许。

预检响应报文

预检响应是服务端对预检请求的回复,浏览器接收后以此为依据判断实际请求是否符合服务端的要求,如果不符合,会提示错误并拒绝实际请求的发送。预检响应包括以下几个字段:

Access-Control-Allow-Origin

和简单请求的响应一样,预检响应会通过此字段告知浏览器服务端是否允许当前域访问其资源。

Access-Control-Allow-Methods

用于说明服务端支持哪些请求方法,其值可以是一个列表,就像上面的实例一样,实际请求的方法必须包含在列表中才能通过预检。

Access-Control-Allow-Headers

用于回复预检请求的 Access-Control-Request-Headers,表明实际请求允许携带的特殊头部字段,同样需要匹配才能通过预检。

Access-Control-Allow-Credentials

用于表示是否允许使用实际请求报文发送 Cookie、Authorization Headers 或者 TLS 客户端证书。

以 Cookie 为例,我们知道,Cookie 也是受浏览器同源策略保护的,想实现 Cookie 的跨域发送,可以通过设置 XMLHttpRequest 的 withCredentials 属性实现(仅对跨域请求有效):

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

当 Access-Control-Allow-Credentials 的值为 true 时,就表示服务端同意此类信息的发送。

另外需要注意的是,如果此字段值为 true,那么 Access-Control-Allow-Origin 的值不能为“*”。

Access-Control-Expose-Headers

默认情况下,在跨域通信中使用 XMLHttpRequest 的 getResponseHeader 方法只能获取响应报文的一些基本字段:Cache-Control、Content-Language、Content-Length、Content-Type、Expires、Last-Modified、Pragma。

如果服务端想在实际响应报文头中加入一些特殊字段,并且希望能在浏览器中获取它们,就可以使用 Access-Control-Expose-Headers 列出这些字段的名称,浏览器收到这样的响应报文后,就会允许这些字段被访问了。

Access-Control-Max-Age

用于指定预检结果能够被缓存多久,单位为秒。

浏览器并不会每次都对相同的请求进行预检,那样效率太低了,预检结果会被缓存,当再有类似的请求,如果与之前的预检匹配,就会像简单请求那样直接发送。

预检完成

浏览器接收到预检响应后,整个预检过程就结束了,浏览器会使用预检结果判断是否发送实际请求。在实例中,预检通过了,于是按照约定发送了实际请求,服务端也正常响应了请求,跨域通信就完成了。

结束啦

到这里,CORS 就介绍完了,你会发现,整个过程是浏览器和服务端自动完成的,基本不需要前端参与了,简直完美。事实上,控制谁能访问自身的资源本来就应该是服务端的职责,在那些黑暗的年代里,前端们殚精竭虑挖空心思想出那么多跨域的方法,甚至掌握多少种跨域方法都成了衡量技术水平的一项标准,后来,CORS 标准出现了,终于解脱了,别再提跨域了,膈应。