CORS-跨域资源共享

CORS(Cross-Origin Resource Sharing)跨域资源共享是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。

跨源 HTTP 请求的一个例子:运行在 https://domain-a.com 的 JavaScript 代码使用 XMLHttpRequest 来发起一个到 https://domain-b.com/data.json 的请求。

出于安全性,浏览器限制脚本内发起的跨源 HTTP 请求。例如,XMLHttpRequest 和 Fetch API 遵循同源策略。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文包含了正确 CORS 响应头。

CORS 机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequest 或 Fetch)使用 CORS,以降低跨源 HTTP 请求所带来的风险。

1.简单请求(Simple requests)

某些请求不会触发 CORS 预检请求。在废弃的 CORS 规范中称这样的请求为简单请求,但是目前的 Fetch 规范(CORS 的现行定义规范)中不再使用这个词语。

其动机是,HTML 4.0 中的 form 元素(早于跨站 XMLHttpRequest 和 fetch)可以向任何来源提交简单请求,所以任何编写服务器的人一定已经在保护跨站请求伪造攻击(CSRF cross-site request forgery)。在这个假设下,服务器不必选择加入(通过响应预检请求 preflight request)来接收任何看起来像表单提交的请求,因为 CSRF 的威胁并不比表单提交的威胁差。

然而,服务器仍然必须提供 Access-Control-Allow-Origin 的选择,以便与脚本共享响应。

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

1.1 使用下列方法之一:
  • GET
  • HEAD
  • POST
1.2 请求头字段

除了被用户代理自动设置的标头字段(例如 Connection、User-Agent 或其他在 Fetch 规范中定义为禁用标头名称的标头),

允许人为设置的字段为 Fetch 规范定义的对 CORS 安全的标头字段集合。该集合为:

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type(需要注意额外的限制)
  • Range(只允许简单的范围标头值 如 bytes=256- 或 bytes=127-255)
1.3 Content-Type

Content-Type 标头所指定的媒体类型的值仅限于下列三者之一:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

1.4 XMLHttpRequest 对象要求

如果请求是使用 XMLHttpRequest 对象发出的,在返回的 XMLHttpRequest.upload 对象属性上没有注册任何事件监听器

也就是说,给定一个 XMLHttpRequest 实例 xhr,没有调用 xhr.upload.addEventListener(),以监听该上传请求。

1.5 请求中没有使用 ReadableStream 对象

如果请求满足上述条件 1.1-1.5:

  • 浏览器将直接发送实际请求,而不需要先发送预检请求。
  • 服务器需要正确配置响应头,以允许来自其他域的跨域请求。

2.简单请求案例

假如站点 https://foo.example 的网页应用想要访问 https://bar.other 的资源。foo.example 的网页中可能包含类似于下面的 JavaScript 代码:

const xhr = new XMLHttpRequest();
const url = "https://bar.other/resources/public-data/";

xhr.open("GET", url);
xhr.onreadystatechange = someHandler;
xhr.send();

此操作实行了客户端和服务器之间的简单交换,使用 CORS 标头字段来处理权限:

服务端返回的 Access-Control-Allow-Origin 标头的 Access-Control-Allow-Origin: * 值表明,该资源可以被任意外源访问。

Alt text

3.预检请求(Preflighted requests)

与简单请求不同,“需预检的请求”要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

如下是一个需要执行预检请求的 HTTP 请求:

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>");

上面的代码使用 POST 请求发送一个 XML 请求体,该请求包含了一个非标准的 HTTP X-PINGOTHER 请求标头。这样的请求标头并不是 HTTP/1.1 的一部分,但通常对于 web 应用很有用处。另外,该请求的 Content-Type 为 application/xml,且使用了自定义的请求标头,所以该请求需要首先发起“预检请求”

Alt text

下面是服务端和客户端完整的信息交互。首次交互是预检请求/响应:

OPTIONS /doc 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
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

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

从上面的报文中,我们看到:

  • 第 1 - 10 行使用 OPTIONS 方法发送了预检请求,浏览器根据上面的 JavaScript 代码片断所使用的请求参数来决定是否需要发送,这样服务器就可以回应是否可以接受用实际的请求参数来发送请求。

  • OPTIONS 是 HTTP/1.1 协议中定义的方法,用于从服务器获取更多信息,是安全的方法。该方法不会对服务器资源产生影响。

  • 注意 OPTIONS 预检请求中同时携带了下面两个标头字段:

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
  • 标头字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。

  • 标头字段 Access-Control-Request-Headers 告知服务器,实际请求将携带两个自定义请求标头字段:X-PINGOTHER 与 Content-Type。服务器据此决定,该实际请求是否被允许。

第 12 - 21 行为预检请求的响应,表明服务器将接受后续的实际请求方法(POST)和请求头(X-PINGOTHER)。 重点看第 15 - 18 行:

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
  • 服务器的响应携带了 Access-Control-Allow-Origin: https://foo.example,从而限制请求的源域。

  • 同时,携带的 Access-Control-Allow-Methods 表明服务器允许客户端使用 POST 和 GET 方法发起请求(与 Allow 响应标头类似,但该标头具有严格的访问控制)。

  • 标头字段 Access-Control-Allow-Headers 表明服务器允许请求中携带字段 X-PINGOTHER 与 Content-Type。

  • 与 Access-Control-Allow-Methods 一样,Access-Control-Allow-Headers 的值为逗号分割的列表。

  • 最后,标头字段 Access-Control-Max-Age 给定了该预检请求可供缓存的时间长短,单位为秒,默认值是 5 秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。

  • 以上例子中,该响应的有效时间为 86400 秒,也就是 24 小时。请注意,浏览器自身维护了一个最大有效时间,如果该标头字段的值超过了最大有效时间,将不会生效。

预检请求完成之后,发送实际请求:

POST /doc 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 XML payload]

4.预检请求与重定向(基本废弃)

5.附带身份凭证的请求

Contributors: --, masecho