CORS 中 Preflight 过程

2017.05.23

两种 CORS 类型

CORS 请求可以分为两种类型

  • 简单的请求
  • 『不是那么简单的请求』

简单的请求需要满足如下标准:

HTTP 请求时下面其中之一(大小写敏感):

  • HEAD
  • GET
  • POST

HTTP 头部与下面匹配(大小写敏感):

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type, 但只能是下面这几个值:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

为什么会简单的请求会有这些特征呢?其实这些请求本身就可以不用脚本直接由浏览器搞定。比如 JSONP 可以直接搞定跨域请求。HTML表单可以弄出 POST 请求。

不满足以上标准的就叫『不是那么简单的请求』。这就需要浏览器和服务器多做点交流。这个多余的交流过程就叫 preflight。之后会详细说明。

处理简单 CORS 请求

假设发起请求的页面是 http://api.bob.com, 用 JavaScript 发出一个跨域请求:

1
2
3
var url = 'http://api.alice.com/cors';
var xhr = createCORSRequest('GET', url);
xhr.send();

请求头部:

1
2
3
4
5
6
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

需要注意的是 CORS 请求 一定 会有 Origin 的 Header。这个 Header 是由浏览器加上的,不能被用户控制。格式是协议 + 域名 + 端口(不是默认端口的话)。

比如:http://api.alice.com

加上这个 Origin Header 也不一定说明这玩意是跨域请求。因为一些同域名的请求也可能有 Origin Header。具体因浏览器而异。但当在同域请求时,浏览器会忽视 CORS 的 response header。

下面是一个有效的 CORS response:

1
2
3
4
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

所有与 CORS 相关的 Headers 都会有 Access-Control- 的前缀。

其中:

Access-Control-Allow-Origin (必须)

这个 Header 必须包含在 CORS 请求的 response 中。如果没有,CORS 请求就会失败。这个 Header 的值可以和 请求的域名一样,也可以为*,如果是后者,所有网站发出的跨域请求都会被满足。

Access-Control-Allow-Credentials (可选)

默认情况下,cookie 是不会包含在 CORS 请求中的,但是设置了这个值后,浏览器发出请求会带上所有目标域名的 cookie,并在得到 response 后设置该域名的 cookie 。这个 Header 唯一的值只能是true,如果不需要 cookie,就不能包含这个 Header,而不是设置值为fasle

这个 Header 需要 XMLHttpRequest 2 对象的 withCredentials 属性设置为 true
两者缺一不可。

Access-Control-Expose-Headers (可选)

如果设置这个属性,那么通过getResponseHeader() 方法可以获得结果简单的 Headers。

允许的值为:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

Access-Control-Expose-Headers的值用逗号分隔。

处理『不是那么简单的请求』

比如请求的方法是 PUT 和 DELETE,或者设置如 Content-Type: application/json, 那么这就是一个『不是那么简单的请求』。

这个过程包含了一个 preflight 的过程。在这个过程中,浏览器和服务器协商,看看服务器是否支持接下来的跨域请求。这个 preflight,就是 OPTIONS 请求。如果协商成功,那么浏览器会真正发出目标请求。preflight 可以被缓存。

发送一个 CORS 请求:

1
2
3
4
5
var url = 'http://api.alice.com/cors';
var xhr = createCORSRequest('PUT', url);
xhr.setRequestHeader(
'X-Custom-Header', 'value');
xhr.send();

Preflight 请求:

1
2
3
4
5
6
7
8
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

CORS 请求的 Header 可以包括下面两个额外的值:

Access-Control-Request-Method

这个 Header 一定会存在,表示实际的请求,即使是简单的请求(GET,POST,HEAD)

Access-Control-Request-Headers

包含在 CORS 请求的 header

上面的那一个 Preflight 请求

1
2
3
4
5
6
7
8
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

Response:

1
2
3
4
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8

Access-Control-Allow-Origin (必须)

同简单的请求。

Access-Control-Allow-Methods (必须)

表示服务器支持的请求方法,逗号分隔。

即使只请求某个特定的方法,然而服务器会返回所有的支持的方法,这个可以方便缓存后不需要额外的 OPTIONS 请求。

Access-Control-Allow-Credentials (可选)

同简单的请求。

Access-Control-Allow-Headers

如果在 OPTIONS 请求中包含了Access-Control-Request-Headers ,那么这个是必须的。这回返回所有服务器支持的 Header,哪怕并没在 preflight 中列出。

Access-Control-Max-Age (可选)

允许 preflight 缓存,之后就可以不需要进行 OPTIONS 请求咯。

当这些条件满足之后,浏览器会发出真正的请求。
比如:

1
2
3
4
5
6
7
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

真的的 response

1
2
Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

如果服务器想拒绝请求,那么只要在 preflight 中不设置 CORS 的 Header 就行了,并返回一个普通的响应码,比如 200。这样浏览器在没有接收到特定的 CORS Header 后,会认为请求无效,不会发出真正的请求。例如:

preflight 请求

1
2
3
4
5
6
7
8
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

Preflight Response:

1
2
// ERROR - No CORS headers, this is an invalid request!
Content-Type: text/html; charset=utf-8

这就会触发 onerror 事件:

1
XMLHttpRequest cannot load http://api.alice.com. Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

节译自:Using CORS