HTTP access control (CORS)
跨域资源共享 CORS 详解

出入安全原因,浏览器限制了脚本跨域 HTTP 请求。一般情况下,当脚本发出跨域 HTTP 请求的时候,浏览器根据返回的头部信息,判断是否拦截返回的结果。通常情况出现错误提示如:
`Access to XMLHttpRequest at ‘xxx’ from origin ‘xxx’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
即,HTTP 请求的返回被浏览器拦截了。在 Chrome 浏览器上可以用插件 ‘Moesif Origin & CORS Changer’ 来处理。

CORS 简介

跨域资源共享 (CORS),是一种在 HTTP 头部附加相应的信息来使浏览器允许 web 应用有权限跨域 HTTP 请求资源的机制。当 HTTP 请求的资源与自身的协议、域名或端口不一样的时候,这个 HTTP 请求就是跨域 HTTP 请求 (cross-origin HTTP request)。如,从前端地址为 http://localhost:8080/ 的应用页面 API 地址为 http://localhost:8000 的数据的时候。

跨域资源共享 (CORS) 机制支持安全的跨域请求,使请求数据可以安全地在浏览器与服务器传输。现代的浏览器可以通过 XMLHttpRequestFetCh API 使用 CORS 机制来处理跨域 HTTP 请求。当然,这些都是浏览器自动处理的,不需要用户参与,而且即使 CORS 请求失败,在 JavaScript 代码层也无法获取错误信息,只能够在浏览器控制台查看。

CORS 标准是通过在 HTTP 头部添加相应的信息来实现跨域资源共享的。浏览器根据头部信息来进行简单请求 (simple requests) 预检请求 (preflighted requests)。

简单请求

CORS 简单请求,即浏览器直接发出 CORS 请求 (在请求头部信息添加 Origin字段) 到服务器,如果响应头部不存在Access-Control-Allow-Origin字段,则说明请求源不在服务器允许的跨域白名单里或服务器不支持跨域请求。

若满足以下所有条件,则该请求为简单请求:

  • 请求方法:

    • HEAD
    • GET
    • POST
  • 请求头部字段不超出:

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type,仅限于:
      • text/plain
      • multipart/form-data
      • application/x-www-form-urlencoded
  • 请求中的任意XMLHttpRequestUpload对象均没有注册任何事件监听器;XMLHttpRequestUpload对象可以使用XMLHttpRequest.upload属性访问。

  • 请求中没有使用 ReadableStream 对象。

CORS 简单请求示例:

1
2
3
4
5
6
GET /api/base/ HTTP/1.1
Host: localhost:8000
Connection: keep-alive
Origin: http://localhost:8080
User-Agent: Mozilla/5.0 ...
Accept-Language: zh-CN,zh;...

响应头部信息:

1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Date: Thu, 27 Dec 2017 20:35:23 GMT
Server: WSGIServer/0.2 CPython/3.6
Content-Type: application/json
Vary: Accept, Cookie, Origin
Allow: GET, HEAD, OPTIONS
X-Frame-Options: SAMEORIGIN
Content-Length: 560
Access-Control-Allow-Credentials: true

响应状态为 200,但是响应头部中没有Access-Control-Allow-Origin,说明服务器不支持跨域 HTTP 请求资源。

当服务器添加跨域请求白名单之后,响应头部信息为:

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
Date: Thu, 27 Dec 2017 20:42:23 GMT
Server: WSGIServer/0.2 CPython/3.6
Content-Type: application/json
Vary: Accept, Cookie, Origin
Allow: GET, HEAD, OPTIONS
X-Frame-Options: SAMEORIGIN
Content-Length: 560
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Expose-Headers: X-CSRFToken, X-SessionId

响应头部Access-Control-Allow-Origin的值为*或请求头部 Origin 字段的值,则说明服务器同意跨域资源共享。


预检请求

预检请求,是在发送真正的跨域请求之前先向服务器发送一个请求方法为 OPTIONS 的 HTTP 请求,来检测服务器是否允许当前请求源的跨域资源请求。

当浏览器发现这满足预检请求条件的时候 (即非简单请求),会自动发出一个预检请求:

1
2
3
4
5
6
7
8
9
10
11
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 ...
Accept-Language: zh-CN,zh;...
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://localhost:8080
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-Custom-Header

  • 预检请求使用OPTIONS方法
  • Origin,和简单请求字段一样,表示请求源;
  • Access-Control-Request-Method,必须字段,告知服务器实际请求将使用的请求方法。
  • Access-Control-Request-Headers:告知服务器,实际请求将携带的额外头部信息。多个头部信息用逗号隔开。

预检请求的响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
HTTP/1.1 200 OK
Date: Thu, 27 Dec 2017 20:55:23 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-CSRFToken, X-SessionId
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 响应,但是没有 CORS 相关的头信息。

当预检请求通过之后,浏览器就会发出实际的跨域资源请求,就像简单请求一样。


Requests with credentials

一般而言,对于跨域XMLHttpRequestFetch请求,浏览器不会发送身份凭证信息。如果要发送附带身份凭证的请求,需要在请求中打开XMLHttpRequestwithCredentials属性。

1
2
3
4
5
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// Vue2 的 axios
import axios from 'axios'
axios.defaults.withCredentials = true;

同时,需要服务器同意发送的请求附带 Cookie。服务器通过响应头部字段Access-Control-Allow-Credentials: true来告诉客户端可以发送 Cookies。

如果服务端的响应头部未携带Access-Control-Allow-Credentials: true,浏览器将不会把响应内容返回给请求的发送者;如果请求端不打开XMLHttpRequestwithCredentials属性,即使服务端同意发送 Cookie,浏览器也不会发送。


The HTTP request headers

  • Origin
    指定请求源 ([scheme]://host[:port],不包含任何路径信息),服务器根据这个值判断是否同意跨域请求。
    注意:请求首部字段 Origin 不是 CORS 专属。

  • Access-Control-Request-Method
    用于预检请求,将实际请求所使用的 HTTP 方法告诉服务器。

  • Access-Control-Request-Headers
    用于预检请求,将实际请求所携带的额外首部字段告诉服务器。

The HTTP response headers

  • Access-Control-Allow-Origin
    用于判断服务器是否同意或支持跨域资源分享。

    • 如果简单请求或预检请求的响应头部不带Access-Control-Allow-Origin字段,则表示服务器不同意跨域请求,或不支持 CORS。即使服务器有正常的响应返回,浏览器为了安全考虑也会自动过滤掉请求的响应结果,抛出被 CORS 策略拦截的相关错误。
    • 如果响应头部有Access-Control-Allow-Origin字段,该字段的值可以为*或请求首部字段Origin的值。当值不是*的时候,响应首部的Vary字段必须包含Origin,表示服务器对不同的请求源会返回不同的内容。
  • Access-Control-Allow-Credentials
    布尔值 (true/false),表示服务器是否允许客户端发送带 Cookie 的 CORS 请求。一般而言,对于跨域XMLHtpRequestFetch请求,浏览器不会发送身份凭证信息。

  • Access-Control-Allow-Methods
    用于预检请求的响应,指明了实际请求所允许使用的 HTTP 方法。

    1
    Access-Control-Allow-Methods: <method>[, <method>]...
  • Access-Control-Allow-Headers
    用于预检请求的响应,指明实际请求中允许携带的头部字段。

    1
    Access-Control-Allow-Headers: <field-name>[, <field-name>]...
  • Access-Control-Expose-Headers
    在跨域访问时,XMLHttpRequest对象的getResponseHeader()方法只能够获取到一些默认允许获取的响应头部字段:Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma。如果想要访问其它头部字段,则需要服务器端设置。如,Django 的corsheaders.middleware.CorsMiddle中间件,通过CORS_EXPOSE_HEADERS = ('X-CSRFToken', 'X-SessionId')来设置。响应头部相应的会出现:

    1
    Access-Control-Expose-Headers: X-CSRFToken, X-SessionId

    这样,客户端就可以获取响应头部字段X-CSRFTokenX-SessionId了。

  • Access-Control-Max-Age
    用于预检请求的响应,告诉浏览器预检请求的结果能够被缓存多久。

    1
    Access-Control-Max-Age: <delta-seconds>

django-cors-headers

Django 可以通过安装django-cors-headers,通过使用相应的中间件来配置处理 CORS 请求。
具体参考文档django-cors-headers