CORS(Cross-Origin Resource Sharing 跨域资源共享)

介绍

实现跨域请求有多种方式, 如使用JSONP, 但由于安全原因, JSONP在使用上有很大限制, 还能通过设置代理, 但这种方式在实现上很繁琐, 而且难以维护.

CORS标准是实现跨域请求比较好的一种方式, 基于XMLHttpRequest对象实现. 想要做出CORS请求, 需要客户端和服务器端合作进行配置.

做出CORS请求

使用JavaScript作出跨域请求:

创建XMLHttpRequest对象

点击查看支持CORS的浏览器列表, Chrome, Firefox, Opera 和 Safari使用XMLHttpRequest2对象, IE使用相类似的XDomainRequest对象, 工作原理基本相似, 不过额外添加了一些安全措施.

首先创建请求对象, Nicholas Zakas 实现了一个简单的辅助函数以区分浏览器的差异:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function createCORSRequest(method, url) {
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr) {

// Check if the XMLHttpRequest object has a "withCredentials" property.
// 检查 XMLHttpRequest 对象含有 withCredentials 属性
// withCredentials 属性只存在于 XMLHTTPRequest2 对象中

xhr.open(method, url, true);

} else if (typeof XDomainRequest != "undefined") {

// XDomainRequest 对象只存在于 IE 浏览器
xhr = new XDomainRequest();
xhr.open(method, url);

} else {

// 该浏览器不支持 CORS
xhr = null;

}
return xhr;
}

var xhr = createCORSRequest('GET', url);
if (!xhr) {
throw new Error('CORS not supported');
}

事件处理器

原始的XMLHttpRequest对象只存在一个事件处理器: onreadystatechange, 会处理所有的响应. 现在onreadystatechange依然可用, 不过XMLHttpRequest2对象引入了许多新的事件处理器. 以下是完整列表:

Event Handler Description
onloadstart* When the request starts.
onprogress While loading and sending data.
onabort* When the request has been aborted. For instance, by invoking the abort() method.
onerror When the request has failed.
onload When the request has successfully completed.
ontimeout When the author specified timeout has passed before the request could complete.
onloadend* When the request has completed (either in success or failure).

*星号表示XDomainRequest不支持, 具体见: 来源

withCredentials

一般来说, CORS请求默认不发送/设置任何cookie. 前端使用xhr.withCredentials = true;以配置包含cookie. 同时服务器这样配置: Access-Control-Allow-Credentials: true.

The .withCredentials property will include any cookies from the remote domain in the request, and it will also set any cookies from the remote domain. 这些cookie仍然遵循同源策略, JS代码无法通过document.cookie或响应头部获取这些cookie. 只能由 remote domain 控制.

发送请求

配置好后就可以发出请求. 使用: xhr.send(), 如果包含请求体, 可在参数中声明xhr.send(body)`.

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Create the XHR object.
function createCORSRequest(method, url) {
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr) {
// XHR for Chrome/Firefox/Opera/Safari.
xhr.open(method, url, true);
} else if (typeof XDomainRequest != "undefined") {
// XDomainRequest for IE.
xhr = new XDomainRequest();
xhr.open(method, url);
} else {
// CORS not supported.
xhr = null;
}
return xhr;
}

// Helper method to parse the title tag from the response.
function getTitle(text) {
return text.match('<title>(.*)?</title>')[1];
}

// Make the actual CORS request.
function makeCorsRequest() {
// This is a sample server that supports CORS.
var url = 'http://html5rocks-cors.s3-website-us-east-1.amazonaws.com/index.html';

var xhr = createCORSRequest('GET', url);
if (!xhr) {
alert('CORS not supported');
return;
}

// Response handlers.
xhr.onload = function() {
var text = xhr.responseText;
var title = getTitle(text);
alert('Response from CORS request to ' + url + ': ' + title);
};

xhr.onerror = function() {
alert('Woops, there was an error making the request.');
};

xhr.send();
}

服务器添加跨域支持

浏览器有时候会添加额外的首部做出额外的请求, 客户端无法知道这些额外的请求, 但是可以使用类似Wireshark 的 packet 分析器发现这些请求. 这些请求是Preflight request(以下译为预先请求).

cors_flow

Preflight request

做出正式CORS请求之前的预先请求, 作用是检查CORS协议是否被理解.

预先请求属于HTTP OPTIONS请求, OPTIONS请求服务器告知其支持的各种功能, 可以询问服务器通常支持哪些方法, 或者对某些特殊资源支持哪些方法. (有些服务器可能只支持对一些特殊类型的对象使用特定的操作). 预先请求首部包括 Access-Control-Request-Method, Access-Control-Request-Headers 以及 Origin.

浏览器会在必要时发出预先请求, 一般情况下, 前端开发者无需在代码中手动添加预先请求.

比如浏览器会在发出DELETE请求之前发出预先请求, 事先检测服务器是否允许DELETE请求, 请求首部内容如下:

1
2
3
4
OPTIONS /resource/foo 
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://foo.bar.org

如果服务器允许DELETE请求, 会对预先请求做出如下的响应, Access-Control-Allow-Methods的值就会包含DELETE方法:

1
2
3
4
5
6
HTTP/1.1 200 OK
Content-Length: 0
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400

CORS Preflight request 示例

1
2
3
4
5
6
7
8
9
10
OPTIONS /resources/post-here/ HTTP/1.1 
Host: bar.other
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
1
2
3
4
5
6
7
8
9
10
11
12
13
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

跨域请求类型

跨域请求有两种类型:

  • 简单跨域请求
  • 复杂跨域请求

处理简单跨域请求

简单跨域请求遵循以下规则:

  • 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

之所以叫简单跨域请求, 是因为这些请求不使用CORS机制也可完成. JSONP请求就属于简单跨域请求(GET). 表单提交也属于简单跨域请求(POST).

观察客户端发出的一个简单请求, 以下是相关JS代码:

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

以下是浏览器发出的HTTP请求, 与CORS相关的首部使用**标记:

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...

合法的跨域请求总是包含Origin头部, 由浏览器自动添加, 用户可以对其进行修改.

所有跨域请求一定都包含Origin首部, 但是并非所有包含Origin首部的请求都是跨域请求, 某些同源请求也会有Origin首部. Firefox浏览器不会对同源请求添加Origin首部, 而Chrome和Safari则会对同源的POST/PUT/DELETE请求添加该首部(同源GET请求没有该首部). 以下是包含Origin首部的一个同源请求例子:

1
2
3
POST /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.bob.com

不管响应是否包含跨域相关的首部, 同源请求的响应都会直接发送给客户端. 尽管如此, 如果Origin的值与所允许的不匹配, 服务器端的代码就会返回错误, 因此要确保允许请求的来源的Origin.

以下是个合法的服务器响应:

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相关的首部前缀均为: "Access-Control-". 以下是各个首部的详细内容:

Access-Control-Allow-Origin(必需) - 所有合法的跨域请求都必须包含该首部, 省略该首部会导致跨域请求失败. 该首部的值可以是HTTP请求中Origin首部的值, 也可以是*以表示允许来自所有origin的请求.

Access-Control-Allow-Credentials(可选) - 默认情况下, 跨域请求不包含cookie. 该首部值若为true, 则请求中就需要包含cookie. 首部唯一合法的值也是true. 如果不需要包含cookie, 正确方式是不要加上这个首部, 而不是将其值设为false.

Access-Control-Allow-Credentials首部和XMLHttpRequest2对象的属性withCredentials结合工作, 如果withCredentials为true, 而响应中不含Access-Control-Allow-Credentials: true首部, 那么该跨域请求则会失败. 反之亦然. 推荐做法是如果不需要cookie, 就不要设置该首部.

Access-Control-Expose-Headers(可选) - XMLHttpRequest 2 对象有一个getResponseHeader()方法, 返回特定的响应首部值. 在执行CORS请求过程中, getResponseHeader()方法只能获取简单响应首部的值, 简单响应首部包括以下几项:

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

如果希望客户端能够获取到其他响应首部的值, 就要使用Access-Control-Expose-Headers - 该首部的值为以逗号分隔的一系列首部名.

处理复杂跨域请求

以上方式处理了简单的GET请求, 但是如果想要处理其他例如PUT, DELETE等请求, 或者希望Content-Type首部的值支持application/json, 就需要处理复杂跨域请求.

复杂跨域请求看似只发送了一个请求, 但是实际上包含两类请求. 浏览器首先发出预先请求, 咨询服务器是否允许发送正式请求, 浏览器会处理这两类请求的具体细节. 预先请求会被缓存, 因此不需要在每一个请求前都发送.

以下是复杂跨域请求的一个例子:

JavaScript:

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 Request:

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...

和简单请求一样, 浏览器会在每一个请求中添加Origin首部, 包括预先请求. 预先请求是HTTP OPTIONS请求(因此要确保服务器能够对该HTTP请求做出响应). OPTIONS也包含几个额外的首部:

Access-Control-Request-Method - 实际HTTP请求的方法, 该首部始终会包含在请求的请求首部, 即使是之前定义的简单HTTP请求(GET, POST, HEAD).

Access-Control-Request-Headers - 该首部的值为以逗号分隔的一系列非简单首部名.

预先请求的作用是检测正式请求是否能够成功被发送. 服务器需要检视这两种请求以确保首部的合法性.

如果HTTP方法和首部都合法, 服务器的请求和响应应如下所示:

Preflight Request:

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
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(必需) - 以逗号分隔的所支持的HTTP方法. 尽管预先请求只是获取许可的一个简单HTTP请求, 但响应首部中必须要列出所有支持的HTTP方法. 这个首部有很大用处, 因为预先请求的响应会存入缓存中, 因此预先响应中可以包含多个请求类型的细节信息.

Access-Control-Allow-Headers(如果请求中包含Access-Control-Request-Headers首部, 则该响应首部必需) - 以逗号分隔的所支持的请求首部, 与Access-Control-Allow-Methods首部的形式很相似, 会列出所有服务器支持的首部(不仅仅是预先请求中所请求的首部).

Access-Control-Allow-Credentials(可选) - 见简单请求.

Access-Control-Max-Age(可选) - 表示预先请求的返回结果(即 Access-Control-Allow-Methods和Access-Control-Allow-Headers提供的信息)可以被缓存多久。

一旦预先请求获取服务器许可, 浏览器就会做出正式请求, 正式请求与简单请求很相似, 响应的处理方式也一样.

正式请求:

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...

正式响应:

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

如果服务器拒绝CORS请求, 则会返回类似HTTP 200的响应, 同时没有任何CORS首部. 如果预先请求中的HTTP方法或首部不合法, 服务器也会拒绝该请求. 如果响应中没有CORS专属的首部, 浏览器会假定该请求不合法, 不发送正式请求:

Preflight Request:

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

如果CORS请求有错误, 浏览器会执行onerror处理器. 在控制台报出如下错误:

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

但浏览器不会报出更详细的错误信息.

CORS请求图示:

cors_server_flowchart

Chrome插件跨域

如果希望Chrome插件支持跨域请求, 可以用以下两种方式:

  1. manifest.json文件中添加如下值:
1
"permissions": [ "http://*.html5rocks.com"]

服务器不需要添加额外CORS首部或者执行任何其他操作.

  1. 如果域名没有在manifest.json文件中声明, 插件就会做出一般的CORS请求, Origin首部的值为"chrome-extension://[CHROME EXTENSION ID]". 此时想要执行跨域请求, 就需要使用如前所述的一般方式.

CORS w/ Images

在Canvas或者WebGL下, 跨域请求图片会引起很大的问题, 可以在中添加crossOrigin属性以解决大部分问题. 具体可以阅读Chromium Blog: Using Cross-domain images in WebGLMozilla Hacks: Using CORS to load WebGL textures from cross-domain images. 阅读CORS enabled image了解其实现方式.

参考