一、前言
跨源资源共享 (CORS) 是一种基于 HTTP 标头的机制,允许服务器指示除其自身之外的任何来源(域、方案或端口),浏览器应允许从中加载资源。 CORS 还依赖于一种机制,浏览器通过该机制向托管跨源资源的服务器发出“预检”请求,以检查服务器是否允许实际请求。在该预检中,浏览器发送指示 HTTP 方法的标头以及将在实际请求中使用的标头。
跨域请求的示例:从 https://domain-a.com
提供的前端 JavaScript 代码使用 XMLHttpRequest
发出对 https://domain-b.com/data.json
的请求。
出于安全原因,浏览器限制从脚本发起的跨域 HTTP 请求。例如, XMLHttpRequest
和 Fetch API 遵循同源策略。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一源请求资源,除非来自其他源的响应包含正确的 CORS 标头。
CORS 机制支持安全的跨域请求以及浏览器和服务器之间的数据传输。现代浏览器在 XMLHttpRequest
或 Fetch 等 API 中使用 CORS 来降低跨源 HTTP 请求的风险。
二、哪些请求使用 CORS?
此跨域共享标准可以启用跨域 HTTP 请求:
- 调用
XMLHttpRequest
或 Fetch API,如上所述。 - Web 字体(用于 CSS 中 @font-face 中的跨域字体使用),以便服务器可以部署只能跨域加载并由允许这样做的网站使用的 TrueType 字体。
- WebGL纹理
- 使用
drawImage()
将图像/视频帧绘制到画布上。 - 来自图像的 CSS 形状。
这是一篇关于跨源资源共享的一般文章,包括对必要的 HTTP 标头的讨论。
三、功能概述
跨源资源共享标准的工作原理是添加新的 HTTP 标头,让服务器描述允许哪些源从 Web 浏览器读取该信息。此外,对于可能对服务器数据造成副作用的 HTTP 请求方法(特别是 GET 或具有某些 MIME 类型的 POST 之外的 HTTP 方法),规范要求浏览器“预检”请求,使用 HTTP OPTIONS 请求方法从服务器请求支持的方法,然后在服务器“批准”后发送实际请求。服务器还可以通知客户端是否应随请求发送“凭据”(例如 Cookie 和 HTTP 身份验证)。
CORS 失败会导致错误,但出于安全原因,JavaScript 无法获取有关错误的详细信息。所有代码都知道发生了错误。确定具体问题的唯一方法是查看浏览器的控制台以获取详细信息。
后续部分讨论场景,并提供所使用的 HTTP 标头的详细信息。
四、访问控制场景示例
我们提出了三个场景来演示跨源资源共享的工作原理。所有这些示例都使用 XMLHttpRequest ,它可以在任何支持的浏览器中发出跨源请求。
4.1 简单的请求
某些请求不会触发 CORS 预检。这些被称为来自过时 CORS 规范的简单请求,尽管 Fetch 规范(现在定义了 CORS)不使用该术语。
动机是 HTML 4.0 中的 <form>
元素(早于跨站点 XMLHttpRequest
和 fetch
)可以向任何来源提交简单的请求,因此任何编写服务器必须已经防止跨站点请求伪造(CSRF)。在这种假设下,服务器不必选择加入(通过响应预检请求)来接收任何看起来像表单提交的请求,因为 CSRF 的威胁并不比表单提交更严重。但是,服务器仍然必须选择使用 Access-Control-Allow-Origin
与脚本共享响应。
简单请求是满足以下所有条件的请求:
- 允许的方法之一:
- 除了用户代理自动设置的标头(例如
Connection
、User-Agent
或 Fetch 规范中定义为禁止标头名称的其他标头)之外,唯一允许手动设置的是 Fetch 规范定义为 CORS 安全列表的请求标头,它们是:Accept
Accept-Language
Content-Language
Content-Type
(请注意下面的附加要求)Range
(仅具有简单的范围标头值;例如bytes=256-
或bytes=127-255
)
- Content-Type 标头中指定的媒体类型允许的唯一类型/子类型组合是:
application/x-www-form-urlencoded
multipart/form-data
text/plain
- 如果使用
XMLHttpRequest
对象发出请求,则不会在请求中使用的XMLHttpRequest.upload
属性返回的对象上注册任何事件侦听器;也就是说,给定XMLHttpRequest
实例xhr
,没有代码调用xhr.upload.addEventListener()
来添加事件侦听器来监视上传。 - 请求中未使用
ReadableStream
对象。
注意:WebKit Nightly 和 Safari 技术预览版对 Accept 、 Accept-Language 和 Content-Language 标头中允许的值施加了额外的限制。如果这些标头中的任何一个具有“非标准”值,WebKit/Safari 不会将该请求视为“简单请求”。 WebKit/Safari 认为“非标准”的值没有记录,除了以下 WebKit bug:
- 需要对非标准 CORS 安全列表请求标头进行预检 Accept、Accept-Language 和 Content-Language
- 对于简单的 CORS,允许在 Accept、Accept-Language 和 Content-Language 请求标头中使用逗号
- 切换到黑名单模型以在简单的 CORS 请求中限制 Accept 标头
没有其他浏览器实现这些额外的限制,因为它们不是规范的一部分。
例如,假设 https://foo.example
处的 Web 内容希望调用域 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 标头来处理权限:
让我们看看在这种情况下浏览器将发送到服务器的内容:
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/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
标头,这意味着该资源可以由任何来源访问。
Access-Control-Allow-Origin: *
Origin
和 Access-Control-Allow-Origin
标头的这种模式是访问控制协议的最简单用法。如果 https://bar.other
的资源所有者希望将对资源的访问限制为仅来自 https://foo.example
的请求(即,除 https://foo.example
之外的任何域都无法访问跨源方式),他们会发送:
Access-Control-Allow-Origin: https://foo.example
Note
注意:当响应凭证请求时,服务器必须在Access-Control-Allow-Origin
标头的值中指定来源,而不是指定“ * ”通配符。
4.2 Preflighted requests 预检请求
与简单请求不同,对于“预检”请求,浏览器首先使用 OPTIONS
方法向其他源上的资源发送 HTTP 请求,以确定实际请求是否可以安全发送。此类跨源请求会进行预检,因为它们可能会对用户数据产生影响。
以下是将预检的请求的示例:
const xhr = new XMLHttpRequest();
xhr.open("POST", "https://bar.other/doc");
xhr.setRequestHeader("X-PINGOTHER", "pingpong");
xhr.setRequestHeader("Content-Type", "text/xml");
xhr.onreadystatechange = handler;
xhr.send("<person><name>Arun</name></person>");
上面的示例创建了一个 XML 正文以与 POST
请求一起发送。此外,还设置了非标准 HTTP X-PINGOTHER
请求标头。此类标头不是 HTTP/1.1 的一部分,但通常对 Web 应用程序有用。由于请求使用 Content-Type
或 text/xml
,并且设置了自定义标头,因此该请求已预检。
Note
注意:如下所述,实际的POST
请求不包含Access-Control-Request-*
标头;仅OPTIONS
请求需要它们。
让我们看一下客户端和服务器之间的完整交换。第一个交换是预检请求/响应:
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 请求之外,还发送了另外两个请求标头(分别为第 9 行和第 10 行):
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
服务器还发送带有值“ X-PINGOTHER, Content-Type
”的 Access-Control-Allow-Headers
,确认这些是允许在实际请求中使用的标头。与 Access-Control-Allow-Methods
一样, Access-Control-Allow-Headers
是一个以逗号分隔的可接受标头列表。
最后, Access-Control-Max-Age
给出了在不发送另一个预检请求的情况下可以缓存对预检请求的响应的时间(以秒为单位)的值。默认值为 5 秒。在本例中,最大期限为 86400 秒(= 24 小时)。请注意,每个浏览器都有一个最大内部值,当 Access-Control-Max-Age
超过该值时,该值优先。
预检请求完成后,将发送真正的请求:
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.3 Preflighted requests and redirects 预检请求和重定向
目前,并非所有浏览器都支持预检请求后的以下重定向。如果在此类请求之后发生重定向,目前某些浏览器会报告如下错误消息:
请求被重定向到“https://example.com/foo”,这对于需要预检的跨源请求是不允许的。请求需要预检,不允许进行跨域重定向。
CORS 协议最初需要这种行为,但随后更改为不再需要它。然而,并非所有浏览器都实现了更改,因此仍然表现出最初所需的行为。
在浏览器赶上规范之前,您可以通过执行以下一项或两项操作来解决此限制:
- 更改服务器端行为以避免预检和/或避免重定向
- 更改请求,使其成为不会导致预检的简单请求
如果这不可能,那么另一种方法是:
- 发出一个简单的请求(使用 Fetch API 的
Response.url
或XMLHttpRequest.responseURL
)来确定实际预检请求最终到达的 URL。 - 使用您在第一步中从
Response.url
或XMLHttpRequest.responseURL
获取的 URL 发出另一个请求(真正的请求)。
但是,如果请求由于请求中存在 Authorization
标头而触发预检,则您将无法使用上述步骤解决该限制。除非您能够控制发出请求的服务器,否则您根本无法解决这个问题。
4.4 带有凭证的请求
注意:当向不同的域发出凭据请求时,第三方 cookie 策略仍然适用。无论服务器和客户端上的任何设置如何,都会始终强制执行该策略,如本章所述。
XMLHttpRequest
或 Fetch 和 CORS 公开的最有趣的功能是能够发出了解 HTTP cookie 和 HTTP 身份验证信息的“凭据”请求。默认情况下,在跨源 XMLHttpRequest
或 Fetch 调用中,浏览器不会发送凭据。调用时,必须在 XMLHttpRequest
对象或 Request
构造函数上设置特定标志。
在此示例中,最初从 https://foo.example
加载的内容向 https://bar.other
上设置 Cookie 的资源发出简单的 GET 请求。 foo.example 上的内容可能包含如下 JavaScript:
const invocation = new XMLHttpRequest();
const url = "https://bar.other/resources/credentialed-content/";
function callOtherDomain() {
if (invocation) {
invocation.open("GET", url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
}
第 7 行显示了 XMLHttpRequest
上的标志,必须设置该标志才能使用 Cookie 进行调用,即 withCredentials
布尔值。默认情况下,调用是在没有 Cookie 的情况下进行的。由于这是一个简单的 GET
请求,因此不会进行预检,但浏览器将拒绝任何没有 Access-Control-Allow-Credentials
: true
标头的响应,并且不会生成可用于调用 Web 内容的响应。
这是客户端和服务器之间的示例交换:
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 payload]
尽管第 10 行包含针对 https://bar.other
上的内容的 Cookie,但如果 bar.other 未使用 Access-Control-Allow-Credentials
: true
进行响应(第 16 行),则响应将为会被忽略并且不会提供给 Web 内容。
4.5 预检请求和凭据
CORS 预检请求绝不能包含凭据。对预检请求的响应必须指定 Access-Control-Allow-Credentials: true
以指示可以使用凭据发出实际请求。
Note
注意:某些企业身份验证服务要求在预检请求中发送 TLS 客户端证书,这违反了 Fetch 规范。Firefox 87 允许通过将首选项设置为
network.cors_preflight.allow_client_cert
到true
来启用此不合规行为(Firefox bug 1511151)。基于 Chromium 的浏览器目前始终在 CORS 预检请求中发送 TLS 客户端证书(Chrome bug 775438)。
4.6 凭证请求和通配符
响应凭证请求时:
- 服务器不得为
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: *
标头(即带有通配符),则浏览器将阻止对响应的访问,并在 devtools 控制台中报告 CORS 错误。
但是,如果请求确实包含凭据(例如 Cookie
标头)并且响应包含实际来源而不是通配符(例如 Access-Control-Allow-Origin: https://example.com
),则浏览器将允许访问来自指定来源的响应。
另请注意,如果响应中的 Access-Control-Allow-Origin
值是“ *
”通配符而不是实际来源,则响应中的任何 Set-Cookie
响应标头都不会设置 cookie 。
4.7 第三方 cookie
请注意,CORS 响应中设置的 cookie 受正常的第三方 cookie 策略的约束。在上面的示例中,页面是从 foo.example
加载的,但第 19 行的 cookie 是由 bar.other
发送的,因此如果用户的浏览器配置为拒绝所有第三方Cookies,则不会保存它。
请求中的 Cookie(第 10 行)也可能在正常的第三方 Cookie 策略中被抑制。因此,强制执行的 cookie 策略可能会使本章中描述的功能无效,从而有效地阻止您发出任何凭据请求。
将应用围绕 SameSite 属性的 Cookie 策略。
五、HTTP 响应标头
本部分列出了服务器针对跨源资源共享规范定义的访问控制请求返回的 HTTP 响应标头。上一节概述了这些操作的实际情况。
5.1 Access-Control-Allow-Origin
返回的资源可能有一个 Access-Control-Allow-Origin
标头,其语法如下:
Access-Control-Allow-Origin: <origin> | *
Access-Control-Allow-Origin
指定单个源,告诉浏览器允许该源访问资源;否则,对于没有凭据的请求,“ *
”通配符告诉浏览器允许任何来源访问该资源。
例如,要允许来自源 https://mozilla.org
的代码访问资源,您可以指定:
Access-Control-Allow-Origin: https://mozilla.org
Vary: Origin
如果服务器指定单个源(可能会根据请求源作为白名单的一部分动态更改)而不是“ *
”通配符,则服务器还应包含 Origin
在 Vary
响应标头中,向客户端指示服务器响应将根据 Origin
请求标头的值而有所不同。
5.2 Access-Control-Expose-Headers
Access-Control-Expose-Headers
标头将指定的标头添加到允许浏览器中的 JavaScript(例如 getResponseHeader()
)访问的白名单中。
Access-Control-Expose-Headers: <header-name>[, <header-name>]*
例如,以下内容:
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
...将允许 X-My-Custom-Header
和 X-Another-Custom-Header
标头暴露给浏览器。
5.3 Access-Control-Max-Age
Access-Control-Max-Age
标头指示预检请求的结果可以缓存多长时间。有关预检请求的示例,请参阅上面的示例。
Access-Control-Max-Age: <delta-seconds>
delta-seconds
参数表示结果可以缓存的秒数。
5.4 Access-Control-Allow-Credentials
Access-Control-Allow-Credentials
标头指示当 credentials
标志为 true 时是否可以公开对请求的响应。当用作预检请求响应的一部分时,这指示是否可以使用凭据发出实际请求。请注意,简单的 GET
请求不会进行预检,因此,如果对带有凭据的资源发出请求,如果此标头未与资源一起返回,则响应将被浏览器忽略,并且不会返回到 Web内容。
Access-Control-Allow-Credentials: true
上面讨论了凭证请求。
5.5 Access-Control-Allow-Methods
Access-Control-Allow-Methods
标头指定访问资源时允许的方法。这用于响应预检请求。上面讨论了请求预检的条件。
Access-Control-Allow-Methods: <method>[, <method>]*
上面给出了预检请求的示例,包括将此标头发送到浏览器的示例。
5.6 Access-Control-Allow-Headers
Access-Control-Allow-Headers
标头用于响应预检请求,以指示在发出实际请求时可以使用哪些 HTTP 标头。该标头是服务器端对浏览器 Access-Control-Request-Headers
标头的响应。
Access-Control-Allow-Headers: <header-name>[, <header-name>]*
六、HTTP 请求标头
本节列出了客户端在发出 HTTP 请求时可以使用的标头,以便利用跨域共享功能。请注意,这些标头是在调用服务器时为您设置的。使用跨源 XMLHttpRequest
功能的开发人员不必以编程方式设置任何跨源共享请求标头。
6.1 Origin
Origin
标头指示跨域访问请求或预检请求的来源。
Origin: <origin>
来源是一个 URL,指示发起请求的服务器。它不包含任何路径信息,仅包含服务器名称。
Note
注意:origin
值可以是null
。
请注意,在任何访问控制请求中,始终发送 Origin
标头。
6.2 Access-Control-Request-Method
Access-Control-Request-Method
在发出预检请求时使用,让服务器知道发出实际请求时将使用什么 HTTP 方法。
Access-Control-Request-Method: <method>
这种用法的示例可以在上面找到。
6.3 Access-Control-Request-Headers
Access-Control-Request-Headers
标头在发出预检请求时使用,让服务器知道发出实际请求时将使用哪些 HTTP 标头(例如使用 setRequestHeader()
)。此浏览器端标头将由 Access-Control-Allow-Headers
的补充服务器端标头进行响应。
Access-Control-Request-Headers: <field-name>[, <field-name>]*
这种用法的示例可以在上面找到。