跨域资源共享 (CORS)

跨域资源共享 (CORS) 是一个基于 HTTP 头部的机制,允许服务器指示任何 (域名、协议或端口)除其自身以外的源,浏览器应该允许从这些源加载资源。 CORS 还依赖于一种机制,浏览器通过这种机制向托管跨域资源的服务器发送“预检”请求,以检查服务器是否允许实际请求。在预检中,浏览器发送标头,指示将用于实际请求的 HTTP 方法和标头。

跨域请求的示例:来自 https://domain-a.com 的前端 JavaScript 代码使用 fetch()https://domain-b.com/data.json 发送请求。

出于安全原因,浏览器限制了从脚本发起的跨域 HTTP 请求。例如,fetch()XMLHttpRequest 遵循 同源策略。这意味着,使用这些 API 的 Web 应用程序只能请求来自其加载应用程序的同一源的资源,除非来自其他源的响应包含正确的 CORS 标头。

Diagrammatic representation of CORS mechanism

CORS 机制支持浏览器和服务器之间安全跨域请求和数据传输。浏览器在 fetch()XMLHttpRequest 等 API 中使用 CORS 来减轻跨域 HTTP 请求的风险。

哪些请求使用 CORS?

这个 跨域共享标准 可以为以下内容启用跨域 HTTP 请求:

这是一篇关于跨域资源共享的通用文章,其中包含对必要 HTTP 头部的讨论。

功能概述

跨域资源共享标准通过添加新的 HTTP 标头 来工作,这些标头让服务器可以描述哪些源被允许从 Web 浏览器读取这些信息。此外,对于可能对服务器数据产生副作用的 HTTP 请求方法(特别是 HTTP 方法,而不是 GET,或带有某些 MIME 类型POST),该规范要求浏览器“预检”请求,使用 HTTP OPTIONS 请求方法从服务器请求支持的方法,然后,在服务器“批准”后,发送实际请求。服务器还可以通知客户端是否应将“凭据”(例如 CookieHTTP 身份验证)与请求一起发送。

CORS 失败会导致错误,但出于安全原因,错误的详细信息不可用于 JavaScript。所有代码都知道的是发生了错误。确定具体错误的唯一方法是查看浏览器的控制台以获取详细信息。

后续部分将讨论场景,并提供对所用 HTTP 头部的细分。

访问控制场景示例

我们展示了三个场景,这些场景演示了跨域资源共享的工作原理。所有这些示例都使用 fetch(),它可以在任何支持的浏览器中发出跨域请求。

简单请求

某些请求不会触发 CORS 预检。这些被称为来自过时的 CORS 规范简单请求,尽管 Fetch 规范(现在定义了 CORS)没有使用该术语。

动机是,来自 HTML 4.0 的 <form> 元素(早于跨站点 fetch()XMLHttpRequest)可以向任何源提交简单请求,因此任何编写服务器的人员都必须已经防止 跨站点请求伪造 (CSRF)。在此假设下,服务器不必选择加入(通过响应预检请求)来接收看起来像表单提交的任何请求,因为 CSRF 的威胁与表单提交的威胁一样糟糕。但是,服务器仍然必须使用 Access-Control-Allow-Origin 选择加入以共享对脚本的响应。

简单请求是满足以下所有条件的请求:

注意: WebKit Nightly 和 Safari Technology Preview 对 AcceptAccept-LanguageContent-Language 标头中允许的值施加了额外的限制。如果任何这些标头具有“非标准”值,WebKit/Safari 不会认为请求是“简单请求”。WebKit/Safari 认为哪些值是“非标准”的没有记录,除了以下 WebKit 错误中:

没有其他浏览器实现这些额外的限制,因为它们不属于规范。

例如,假设位于 https://foo.example 的 Web 内容希望从域名 https://bar.other 获取 JSON 内容。这种代码可能会用于部署在 foo.example 上的 JavaScript:

js
const fetchPromise = fetch("https://bar.other");

fetchPromise
  .then((response) => response.json())
  .then((data) => {
    console.log(data);
  });

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

Diagram of simple CORS GET request

让我们看看在这种情况下,浏览器将发送给服务器的内容:

http
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

值得注意的请求标头是 Origin,它表明调用来自 https://foo.example

现在让我们看看服务器如何响应:

http
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…]

作为响应,服务器返回一个带有 Access-Control-Allow-Origin: *Access-Control-Allow-Origin 标头,这意味着该资源可以被任何源访问。

http
Access-Control-Allow-Origin: *

这种 OriginAccess-Control-Allow-Origin 标头的模式是访问控制协议的最简单用法。如果 https://bar.other 的资源所有者希望将对资源的访问权限限制为来自 https://foo.example 的请求(即,除了 https://foo.example 之外的任何域名都无法以跨域方式访问该资源),他们将发送:

http
Access-Control-Allow-Origin: https://foo.example

注意:当响应 有凭据的请求 请求时,服务器必须Access-Control-Allow-Origin 标头的值中指定一个源,而不是指定“*”通配符。

预检请求

简单请求 不同,对于“预检”请求,浏览器首先使用 OPTIONS 方法向另一个源上的资源发送 HTTP 请求,以确定是否可以安全发送实际请求。这种跨域请求是预检的,因为它们可能对用户数据有影响。

以下是一个将被预检的请求示例:

js
const fetchPromise = fetch("https://bar.other/doc", {
  method: "POST",
  mode: "cors",
  headers: {
    "Content-Type": "text/xml",
    "X-PINGOTHER": "pingpong",
  },
  body: "<person><name>Arun</name></person>",
});

fetchPromise.then((response) => {
  console.log(response.status);
});

上面的示例创建了一个 XML 主体,并将其与 POST 请求一起发送。此外,还设置了一个非标准的 HTTP X-PINGOTHER 请求标头。这些标头不属于 HTTP/1.1,但通常对 Web 应用程序很有用。由于该请求使用的 Content-Typetext/xml,并且设置了自定义标头,因此该请求将被预检。

Diagram of a request that is preflighted

注意:如以下所述,实际的POST请求不包含Access-Control-Request-*标头;它们仅在OPTIONS请求中需要。

让我们看一下客户端和服务器之间完整的交互。第一次交互是预检请求/响应

http
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: content-type,x-pingother

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

上面第一个块代表预检请求,使用OPTIONS方法。浏览器根据上面 JavaScript 代码片段使用的请求参数确定它需要发送此请求,以便服务器能够响应是否允许使用实际请求参数发送请求。OPTIONS 是 HTTP/1.1 方法,用于从服务器获取更多信息,并且是一个安全方法,这意味着它不能用于更改资源。请注意,除了 OPTIONS 请求之外,还会发送另外两个请求标头。

http
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-pingother

Access-Control-Request-Method标头作为预检请求的一部分通知服务器,当实际请求发送时,它将使用POST请求方法。 Access-Control-Request-Headers标头通知服务器,当实际请求发送时,它将使用X-PINGOTHERContent-Type自定义标头。现在服务器有机会确定它是否可以接受这些条件下的请求。

上面的第二个块是服务器返回的响应,它表明请求方法(POST)和请求标头(X-PINGOTHER)是可以接受的。让我们更仔细地看一下以下几行。

http
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进行响应,表明POSTGET是查询所述资源的有效方法(此标头类似于Allow响应标头,但仅在访问控制的上下文中使用)。

服务器还发送Access-Control-Allow-Headers,其值为“X-PINGOTHER, Content-Type”,确认这些是允许与实际请求一起使用的标头。与Access-Control-Allow-Methods类似,Access-Control-Allow-Headers是一个逗号分隔的允许标头的列表。

最后,Access-Control-Max-Age以秒为单位提供预检请求响应可以缓存而不发送其他预检请求的时长。默认值为 5 秒。在本例中,最大年龄为 86400 秒(= 24 小时)。请注意,每个浏览器都有一个最大内部值,当Access-Control-Max-Age超过此值时,该值优先。

预检请求完成后,会发送实际请求。

http
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 content]

预检请求和重定向

并非所有浏览器当前都支持在预检请求后进行重定向。如果在这样的请求后发生重定向,一些浏览器当前会报告以下错误消息。

请求重定向到https://example.com/foo,这对于需要预检的跨域请求是不允许的。请求需要预检,这在跨域重定向中是不允许的。

CORS 协议最初要求这种行为,但随后被更改为不再要求它。但是,并非所有浏览器都已实现此更改,因此仍然表现出最初要求的行为。

在浏览器赶上规范之前,您可以通过以下一项或两项操作来解决此限制。

  • 更改服务器端行为以避免预检和/或避免重定向。
  • 更改请求,使其成为简单请求,不会导致预检。

如果这不可行,则另一种方法是:

  1. 发出简单请求(对于 Fetch API 使用Response.url,或对于 XMLHttpRequest 使用XMLHttpRequest.responseURL)来确定实际预检请求最终将到达哪个 URL。
  2. 使用在第一步中从Response.urlXMLHttpRequest.responseURL获得的 URL,发出另一个请求(实际请求)。

但是,如果请求是在请求中存在Authorization标头的情况下触发预检的请求,则无法使用上述步骤来解决此限制。除非您控制发出请求的服务器,否则您根本无法解决此限制。

包含凭据的请求

注意:当向不同域发出包含凭据的请求时,第三方 cookie 策略仍然适用。该策略始终强制执行,而与本章所述的服务器和客户端上的任何设置无关。

fetch()XMLHttpRequest和 CORS 公开的最有趣的功能是能够发出“包含凭据”的请求,这些请求能够感知HTTP cookie和 HTTP 身份验证信息。默认情况下,在跨域fetch()XMLHttpRequest调用中,浏览器不会发送凭据。

要请求fetch()请求包含凭据,请将credentials选项设置为"include"

要请求XMLHttpRequest请求包含凭据,请将XMLHttpRequest.withCredentials属性设置为true

在此示例中,最初从https://foo.example加载的内容向https://bar.other上的资源发出一个简单的 GET 请求,该资源会设置 Cookie。foo.example 上的内容可能包含以下 JavaScript 代码。

js
const url = "https://bar.other/resources/credentialed-content/";

const request = new Request(url, { credentials: "include" });

const fetchPromise = fetch(request);
fetchPromise.then((response) => console.log(response));

此代码创建一个Request对象,在构造函数中将credentials选项设置为"include",然后将此请求传递到fetch()。由于这是一个简单的GET请求,因此不会进行预检,但浏览器会拒绝任何没有Access-Control-Allow-Credentials: true标头的响应,并且不会将响应提供给调用的 Web 内容。

Diagram of a simple GET request with Access-Control-Allow-Credentials

以下是一个客户端和服务器之间的示例交互。

http
GET /resources/credentialed-content/ 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
Referer: https://foo.example/examples/credential.html
Origin: https://foo.example
Cookie: pageAccess=2

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

[text/plain content]

尽管请求的Cookie标头包含发往https://bar.other上的内容的 cookie,但如果 bar.other 没有使用Access-Control-Allow-Credentials(值为true)进行响应(如本示例所示),则该响应将被忽略,不会提供给 Web 内容。

预检请求和凭据

CORS 预检请求决不包含凭据。预检请求的响应必须指定Access-Control-Allow-Credentials: true,以表明实际请求可以使用凭据发出。

注意:一些企业身份验证服务要求在预检请求中发送 TLS 客户端证书,这与Fetch规范相违背。

Firefox 87 允许通过将首选项设置为:network.cors_preflight.allow_client_certtrue来启用这种不符合规范的行为(Firefox bug 1511151)。基于 Chromium 的浏览器目前始终在 CORS 预检请求中发送 TLS 客户端证书(Chrome bug 775438)。

包含凭据的请求和通配符

当响应包含凭据的请求时

  • 服务器不能Access-Control-Allow-Origin响应标头值指定“*”通配符,而必须指定明确的源;例如:Access-Control-Allow-Origin: https://example.com
  • 服务器不能Access-Control-Allow-Headers响应标头值指定“*”通配符,而必须指定明确的标头名称列表;例如,Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
  • 服务器不能Access-Control-Allow-Methods响应标头值指定“*”通配符,而必须指定明确的方法名称列表;例如,Access-Control-Allow-Methods: POST, GET
  • 服务器不能Access-Control-Expose-Headers响应标头值指定“*”通配符,而必须指定明确的标头名称列表;例如,Access-Control-Expose-Headers: Content-Encoding, Kuma-Revision

如果请求包含凭据(最常见的是Cookie标头),并且响应包含Access-Control-Allow-Origin: *标头(即使用通配符),则浏览器会阻止访问响应,并在开发者工具控制台中报告 CORS 错误。

但是,如果请求确实包含凭据(例如Cookie标头),并且响应包含实际源而不是通配符(例如,Access-Control-Allow-Origin: https://example.com),则浏览器会允许从指定源访问响应。

另外请注意,如果响应中的Set-Cookie响应标头中的Access-Control-Allow-Origin值为“*”通配符而不是实际源,则不会设置 cookie。

第三方 cookie

请注意,在 CORS 响应中设置的 cookie 遵守正常的第三方 cookie 策略。在上例中,页面是从foo.example加载的,但响应中的Cookie标头是由bar.other发送的,因此如果用户的浏览器配置为拒绝所有第三方 cookie,则不会保存该 cookie。

请求中的 cookie 也可能在正常的第三方 cookie 策略中被抑制。因此,强制执行的 cookie 策略可能会使本章中描述的功能失效,从而有效地阻止您发出包含凭据的请求。

SameSite属性周围的 cookie 策略将适用。

HTTP 响应头

本节列出了服务器根据跨域资源共享规范返回的访问控制请求的 HTTP 响应标头。上一节概述了这些标头在实际情况中的应用。

Access-Control-Allow-Origin

返回的资源可以有一个Access-Control-Allow-Origin标头,其语法如下。

http
Access-Control-Allow-Origin: <origin> | *

Access-Control-Allow-Origin指定单个源,告诉浏览器允许该源访问资源;或者 - 对于不含凭据的请求 - “*”通配符告诉浏览器允许任何源访问资源。

例如,要允许来自源https://mozilla.org的代码访问资源,您可以指定以下内容。

http
Access-Control-Allow-Origin: https://mozilla.org
Vary: Origin

如果服务器指定单个源(该源可以根据请求源动态更改,作为允许列表的一部分),而不是“*”通配符,那么服务器还应该在Vary响应标头中包含Origin,以告知客户端服务器响应将根据Origin请求标头的值而有所不同。

Access-Control-Expose-Headers

Access-Control-Expose-Headers标头将指定标头添加到允许列表中,浏览器中的 JavaScript(例如Response.headers)可以访问这些标头。

http
Access-Control-Expose-Headers: <header-name>[, <header-name>]*

例如,以下内容

http
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header

…将允许X-My-Custom-HeaderX-Another-Custom-Header标头公开给浏览器。

Access-Control-Max-Age

Access-Control-Max-Age标头指示预检请求的结果可以缓存多长时间。有关预检请求的示例,请参见上面的示例。

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

delta-seconds参数指示结果可以缓存的秒数。

Access-Control-Allow-Credentials

Access-Control-Allow-Credentials 头部指示当 credentials 标志为真时,对请求的响应是否可以公开。当用作对预检请求的响应的一部分时,这表示是否可以使用凭据发出实际请求。请注意,简单的 GET 请求不会进行预检,因此,如果使用凭据对资源进行请求,但没有使用此头部返回资源,则浏览器会忽略响应,不会将其返回给网页内容。

http
Access-Control-Allow-Credentials: true

带凭据的请求 在上面讨论过。

Access-Control-Allow-Methods

Access-Control-Allow-Methods 头部指定访问资源时允许的方法。这在响应预检请求时使用。对预检请求进行条件的讨论在上面。

http
Access-Control-Allow-Methods: <method>[, <method>]*

预检请求 的示例在上面给出,包括向浏览器发送此头部的示例。

Access-Control-Allow-Headers

Access-Control-Allow-Headers 头部用于响应预检请求 ,以指示在发出实际请求时可以使用哪些 HTTP 头部。此头部是服务器端对浏览器端Access-Control-Request-Headers 头部的响应。

http
Access-Control-Allow-Headers: <header-name>[, <header-name>]*

HTTP 请求头

本节列出了客户端在发出 HTTP 请求时可以使用的头部,以利用跨域共享功能。请注意,在向服务器发出调用时,这些头部将为您设置。进行跨域请求的开发人员不需要以编程方式设置任何跨域共享请求头部。

Origin

Origin 头部指示跨域访问请求或预检请求的来源。

http
Origin: <origin>

来源是一个 URL,指示发起请求的服务器。它不包含任何路径信息,只有服务器名称。

注意:origin 值可以为 null

请注意,在任何访问控制请求中,Origin 头部都会始终发送。

Access-Control-Request-Method

Access-Control-Request-Method 用于在发出预检请求时,让服务器知道在发出实际请求时将使用什么 HTTP 方法。

http
Access-Control-Request-Method: <method>

此用法的示例可以在上面找到。

Access-Control-Request-Headers

Access-Control-Request-Headers 头部用于在发出预检请求时,让服务器知道在发出实际请求时将使用哪些 HTTP 头部(例如,通过将它们作为headers 选项传递)。此浏览器端头部将由服务器端互补的Access-Control-Allow-Headers 头部进行响应。

http
Access-Control-Request-Headers: <field-name>[,<field-name>]*

此用法的示例可以在上面找到

规范

规范
Fetch 标准
# http-access-control-allow-origin

浏览器兼容性

BCD 表格仅在浏览器中加载

另请参阅