为了安全,W3C 定义了 同源策略,使得一些场景下,在网站 A 下的代码无法直接访问网站 B 的资源。HTTP 中定义的 CORS,则作为一种控制机制来让服务端限定客户端可以访问哪些资源。
哪些场景适用于 CORS?
CORS 限制的场景有这些:
- Invocations of the XMLHttpRequest or Fetch APIs, as discussed above.
- Web Fonts (for cross-domain font usage in
@font-face
within CSS), so that servers can deploy TrueType fonts that can only be cross-site loaded and used by web sites that are permitted to do so. - WebGL textures.
- Images/video frames drawn to a canvas using drawImage().
- CSS Shapes from images.
CORS 的简单请求
当你从 foo.example
向 bar.other
发起一个异步请求时(用 XMLHttpRequest
或者 Fetch API),浏览器会根据回包中的 Access-Control-*
头来判断这个请求是否成功:
# 请求
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
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
Connection: keep-alive
Origin: https://foo.example
# 回复
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
[…XML Data…]
重点是:
- 浏览器会自动给请求包带上
Origin
头表示发起请求的源 - 服务器回包中的
Access-Control-Allow-Origin
头,表示服务端接受任意的来源网站来请求;它也可以是一个具体的 源,如https://foo.example
Preflight request
CORS 这套机制考虑了安全性,对于不是「简单请求」的情况,
CORS 规范中定义了 简单请求 的概念。它是指接口行为相对「简单」的情形,要求:
- HTTP 方法是
GET
、HEAD
或POST
- HTTP 头中仅有限定的一批 header;比如不能有自定义的
X-*
header - 当
POST
场景时,Content-Type
只能是下面其一。这意味着你不能 POST 一个 JSON 的数据:- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- 不能使用
XMLHttpRequest.upload
或ReadableStream
如果不是简单请求,那意味着你的请求可能 不常见或者行为复杂,带有一定的危险。浏览器会使用 OPTION
方法先发送一个 preflight 请求服务端来确认它是否可以发送这个请求,然后再发送实际的请求。例如:
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://bar.other/resources/post-here/');
xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');
这段代码中使用了自定义的 X-PINGOTHER
头,以及 Content-Type
头为「简单请求」定义之外的 application/xml
,因此浏览器会先发 preflight 请求:
# Preflight 请求
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
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
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
# Preflight 回包
# Access-Control-Allow-Headers 表示这些头是可以被使用的
# Access-Control-Max-Age 表示这个回包的信息可以用一天
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://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
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
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
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: https://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: https://foo.example
Pragma: no-cache
Cache-Control: no-cache
<person><name>Arun</name></person>
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain
[Some GZIP'd payload]
可以看出,CORS 是需要客户端和服务端互相配合的机制。
Requests with credentials
如果你希望跨域请求带上对方网站的身份信息(cookie 或者 basic auth),那你可以用 XMLHttpRequest
或者 Fetch 接口中提供的控制字段 withCredentials
:
const invocation = new XMLHttpRequest();
const url = 'http://bar.other/resources/credentialed-content/';
function callOtherDomain() {
if (invocation) {
invocation.open('GET', url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
}
带不带 withCredentials
并 不影响 浏览器判断做不做 preflight。但是如果请求的回包中没有 Access-Control-Allow-Credentials: true
头,则浏览器会丢弃该回包并报错。
回包中的其他头字段
除了上述已经出现的头字段,还有一些字段带有控制作用:
Access-Control-Expose-Headers
- 表示客户端代码可以看到的、除了 CORS 定义的一个头列表之外的头字段。比如
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
表示客户端代码可以看到服务器返回的X-My-Custom-Header
和X-Another-Custom-Header