跨源资源共享(CORS)

Baseline 已广泛支持

此特性已相当成熟,可在许多设备和浏览器版本上使用。自 ⁨2015 年 7 月⁩以来,各浏览器均已提供此特性。

跨域资源共享 (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 请求:

  • 如上所述,调用 fetch()XMLHttpRequest
  • Web 字体(用于 CSS 中 @font-face 的跨域字体使用),如字体获取要求中所述,以便服务器可以部署 TrueType 字体,这些字体只能跨域加载并由被允许的网站使用。
  • WebGL 纹理.
  • 使用 drawImage() 绘制到 canvas 的图像/视频帧。
  • 来自图像的 CSS Shapes。

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

功能概述

跨域资源共享标准通过添加新的 HTTP 标头来实现,这些标头允许服务器描述哪些源被允许从 Web 浏览器读取该信息。此外,对于可能对服务器数据造成副作用的 HTTP 请求方法(特别是除了 GET 之外的 HTTP 方法,或具有某些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.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 请求,该资源设置了 Cookies。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 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 未响应带有值 trueAccess-Control-Allow-Credentials,如本例所示,则响应将被忽略,并且不提供给 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),那么浏览器将允许从指定的源访问响应。

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

第三方 cookie

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

CORS 请求和响应中设置的 cookie 受到正常的第三方 cookie 策略的约束。

第三方 cookie 策略可能会阻止在请求中发送第三方 cookie,从而有效地阻止站点发出带凭据的请求,即使第三方服务器允许(使用 Access-Control-Allow-Credentials)。不同浏览器之间的默认策略不同,但可以使用 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>]*

例如,以下将允许 X-My-Custom-HeaderX-Another-Custom-Header 标头暴露给浏览器:

http
Access-Control-Expose-Headers: X-My-Custom-Header, X-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 标志为 true 时,请求的响应是否可以暴露。当作为预检请求响应的一部分使用时,这表示实际请求是否可以使用凭据进行。请注意,简单的 GET 请求不会进行预检,因此如果对带凭据的资源发出请求,如果资源没有返回此标头,则浏览器将忽略响应,并且不会将其返回给 Web 内容。

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 标头(例如,通过将它们作为 headers 选项传递)。此浏览器端标头将由服务器端补充标头 Access-Control-Allow-Headers 响应。

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

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

浏览器兼容性

另见