跨域资源共享 (CORS)
跨域资源共享 (CORS) 是一个基于 HTTP 头部的机制,允许服务器指示任何 源(域名、协议或端口)除其自身以外的源,浏览器应该允许从这些源加载资源。 CORS 还依赖于一种机制,浏览器通过这种机制向托管跨域资源的服务器发送“预检”请求,以检查服务器是否允许实际请求。在预检中,浏览器发送标头,指示将用于实际请求的 HTTP 方法和标头。
跨域请求的示例:来自 https://domain-a.com
的前端 JavaScript 代码使用 fetch()
向 https://domain-b.com/data.json
发送请求。
出于安全原因,浏览器限制了从脚本发起的跨域 HTTP 请求。例如,fetch()
和 XMLHttpRequest
遵循 同源策略。这意味着,使用这些 API 的 Web 应用程序只能请求来自其加载应用程序的同一源的资源,除非来自其他源的响应包含正确的 CORS 标头。
CORS 机制支持浏览器和服务器之间安全跨域请求和数据传输。浏览器在 fetch()
或 XMLHttpRequest
等 API 中使用 CORS 来减轻跨域 HTTP 请求的风险。
哪些请求使用 CORS?
这个 跨域共享标准 可以为以下内容启用跨域 HTTP 请求:
- 如上所述,
fetch()
或XMLHttpRequest
的调用。 - Web 字体(用于在 CSS 中的
@font-face
中跨域使用字体),以便服务器可以部署只能跨域加载的 TrueType 字体,并被允许使用的网站使用。 - WebGL 纹理.
- 使用
drawImage()
绘制到画布的图像/视频帧。 - 来自图像的 CSS 形状。
这是一篇关于跨域资源共享的通用文章,其中包含对必要 HTTP 头部的讨论。
功能概述
跨域资源共享标准通过添加新的 HTTP 标头 来工作,这些标头让服务器可以描述哪些源被允许从 Web 浏览器读取这些信息。此外,对于可能对服务器数据产生副作用的 HTTP 请求方法(特别是 HTTP 方法,而不是 GET
,或带有某些 MIME 类型 的 POST
),该规范要求浏览器“预检”请求,使用 HTTP OPTIONS
请求方法从服务器请求支持的方法,然后,在服务器“批准”后,发送实际请求。服务器还可以通知客户端是否应将“凭据”(例如 Cookie 和 HTTP 身份验证)与请求一起发送。
CORS 失败会导致错误,但出于安全原因,错误的详细信息不可用于 JavaScript。所有代码都知道的是发生了错误。确定具体错误的唯一方法是查看浏览器的控制台以获取详细信息。
后续部分将讨论场景,并提供对所用 HTTP 头部的细分。
访问控制场景示例
我们展示了三个场景,这些场景演示了跨域资源共享的工作原理。所有这些示例都使用 fetch()
,它可以在任何支持的浏览器中发出跨域请求。
简单请求
某些请求不会触发 CORS 预检。这些被称为来自过时的 CORS 规范 的简单请求,尽管 Fetch 规范(现在定义了 CORS)没有使用该术语。
动机是,来自 HTML 4.0 的 <form>
元素(早于跨站点 fetch()
和 XMLHttpRequest
)可以向任何源提交简单请求,因此任何编写服务器的人员都必须已经防止 跨站点请求伪造 (CSRF)。在此假设下,服务器不必选择加入(通过响应预检请求)来接收看起来像表单提交的任何请求,因为 CSRF 的威胁与表单提交的威胁一样糟糕。但是,服务器仍然必须使用 Access-Control-Allow-Origin
选择加入以共享对脚本的响应。
简单请求是满足以下所有条件的请求:
- 允许的方法之一
- 除了用户代理自动设置的标头(例如,
Connection
、User-Agent
,或 Fetch 规范中定义的其他标头作为禁止的标头名称)之外,唯一允许手动设置的标头是 Fetch 规范定义为 CORS-safelisted 请求标头的那些标头,它们是: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 Technology Preview 对 Accept
、Accept-Language
和 Content-Language
标头中允许的值施加了额外的限制。如果任何这些标头具有“非标准”值,WebKit/Safari 不会认为请求是“简单请求”。WebKit/Safari 认为哪些值是“非标准”的没有记录,除了以下 WebKit 错误中:
- 对于非标准 CORS-safelisted 请求标头 Accept、Accept-Language 和 Content-Language,需要预检
- 对于简单的 CORS,允许在 Accept、Accept-Language 和 Content-Language 请求标头中使用逗号
- 在简单的 CORS 请求中,切换到针对受限 Accept 标头的黑名单模型
没有其他浏览器实现这些额外的限制,因为它们不属于规范。
例如,假设位于 https://foo.example
的 Web 内容希望从域名 https://bar.other
获取 JSON 内容。这种代码可能会用于部署在 foo.example
上的 JavaScript:
const fetchPromise = fetch("https://bar.other");
fetchPromise
.then((response) => response.json())
.then((data) => {
console.log(data);
});
此操作在客户端和服务器之间执行简单的交换,使用 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
注意:当响应 有凭据的请求 请求时,服务器必须在 Access-Control-Allow-Origin
标头的值中指定一个源,而不是指定“*
”通配符。
预检请求
与 简单请求 不同,对于“预检”请求,浏览器首先使用 OPTIONS
方法向另一个源上的资源发送 HTTP 请求,以确定是否可以安全发送实际请求。这种跨域请求是预检的,因为它们可能对用户数据有影响。
以下是一个将被预检的请求示例:
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-Type
是 text/xml
,并且设置了自定义标头,因此该请求将被预检。
注意:如以下所述,实际的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: 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 请求之外,还会发送另外两个请求标头。
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-pingother
Access-Control-Request-Method
标头作为预检请求的一部分通知服务器,当实际请求发送时,它将使用POST
请求方法。 Access-Control-Request-Headers
标头通知服务器,当实际请求发送时,它将使用X-PINGOTHER
和Content-Type
自定义标头。现在服务器有机会确定它是否可以接受这些条件下的请求。
上面的第二个块是服务器返回的响应,它表明请求方法(POST
)和请求标头(X-PINGOTHER
)是可以接受的。让我们更仔细地看一下以下几行。
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 小时)。请注意,每个浏览器都有一个最大内部值,当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 content]
预检请求和重定向
并非所有浏览器当前都支持在预检请求后进行重定向。如果在这样的请求后发生重定向,一些浏览器当前会报告以下错误消息。
请求重定向到
https://example.com/foo
,这对于需要预检的跨域请求是不允许的。请求需要预检,这在跨域重定向中是不允许的。
CORS 协议最初要求这种行为,但随后被更改为不再要求它。但是,并非所有浏览器都已实现此更改,因此仍然表现出最初要求的行为。
在浏览器赶上规范之前,您可以通过以下一项或两项操作来解决此限制。
- 更改服务器端行为以避免预检和/或避免重定向。
- 更改请求,使其成为简单请求,不会导致预检。
如果这不可行,则另一种方法是:
- 发出简单请求(对于 Fetch API 使用
Response.url
,或对于 XMLHttpRequest 使用XMLHttpRequest.responseURL
)来确定实际预检请求最终将到达哪个 URL。 - 使用在第一步中从
Response.url
或XMLHttpRequest.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 代码。
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 内容。
以下是一个客户端和服务器之间的示例交互。
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_cert
为true
来启用这种不符合规范的行为(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
标头,其语法如下。
Access-Control-Allow-Origin: <origin> | *
Access-Control-Allow-Origin
指定单个源,告诉浏览器允许该源访问资源;或者 - 对于不含凭据的请求 - “*
”通配符告诉浏览器允许任何源访问资源。
例如,要允许来自源https://mozilla.org
的代码访问资源,您可以指定以下内容。
Access-Control-Allow-Origin: https://mozilla.org
Vary: Origin
如果服务器指定单个源(该源可以根据请求源动态更改,作为允许列表的一部分),而不是“*
”通配符,那么服务器还应该在Vary
响应标头中包含Origin
,以告知客户端服务器响应将根据Origin
请求标头的值而有所不同。
Access-Control-Expose-Headers
Access-Control-Expose-Headers
标头将指定标头添加到允许列表中,浏览器中的 JavaScript(例如Response.headers
)可以访问这些标头。
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
标头公开给浏览器。
Access-Control-Max-Age
Access-Control-Max-Age
标头指示预检请求的结果可以缓存多长时间。有关预检请求的示例,请参见上面的示例。
Access-Control-Max-Age: <delta-seconds>
delta-seconds
参数指示结果可以缓存的秒数。
Access-Control-Allow-Credentials
Access-Control-Allow-Credentials
头部指示当 credentials
标志为真时,对请求的响应是否可以公开。当用作对预检请求的响应的一部分时,这表示是否可以使用凭据发出实际请求。请注意,简单的 GET
请求不会进行预检,因此,如果使用凭据对资源进行请求,但没有使用此头部返回资源,则浏览器会忽略响应,不会将其返回给网页内容。
Access-Control-Allow-Credentials: true
带凭据的请求 在上面讨论过。
Access-Control-Allow-Methods
Access-Control-Allow-Methods
头部指定访问资源时允许的方法。这在响应预检请求时使用。对预检请求进行条件的讨论在上面。
Access-Control-Allow-Methods: <method>[, <method>]*
预检请求 的示例在上面给出,包括向浏览器发送此头部的示例。
Access-Control-Allow-Headers
Access-Control-Allow-Headers
头部用于响应预检请求 ,以指示在发出实际请求时可以使用哪些 HTTP 头部。此头部是服务器端对浏览器端Access-Control-Request-Headers
头部的响应。
Access-Control-Allow-Headers: <header-name>[, <header-name>]*
HTTP 请求头
本节列出了客户端在发出 HTTP 请求时可以使用的头部,以利用跨域共享功能。请注意,在向服务器发出调用时,这些头部将为您设置。进行跨域请求的开发人员不需要以编程方式设置任何跨域共享请求头部。
Origin
Origin
头部指示跨域访问请求或预检请求的来源。
Origin: <origin>
来源是一个 URL,指示发起请求的服务器。它不包含任何路径信息,只有服务器名称。
注意:origin
值可以为 null
。
请注意,在任何访问控制请求中,Origin
头部都会始终发送。
Access-Control-Request-Method
Access-Control-Request-Method
用于在发出预检请求时,让服务器知道在发出实际请求时将使用什么 HTTP 方法。
Access-Control-Request-Method: <method>
此用法的示例可以在上面找到。
Access-Control-Request-Headers
Access-Control-Request-Headers
头部用于在发出预检请求时,让服务器知道在发出实际请求时将使用哪些 HTTP 头部(例如,通过将它们作为headers
选项传递)。此浏览器端头部将由服务器端互补的Access-Control-Allow-Headers
头部进行响应。
Access-Control-Request-Headers: <field-name>[,<field-name>]*
此用法的示例可以在上面找到。
规范
规范 |
---|
Fetch 标准 # http-access-control-allow-origin |
浏览器兼容性
BCD 表格仅在浏览器中加载
另请参阅
- CORS 错误
- 启用 CORS:我要在我的服务器上添加 CORS 支持
- Fetch API
XMLHttpRequest
- Will it CORS? - CORS 解释器和生成器
- 如何在没有 CORS 的情况下运行 Chrome 浏览器
- 如何在所有(现代)浏览器中使用 CORS
- Stack Overflow 答案,包含处理常见问题的“操作方法”信息:
- 如何避免 CORS 预检
- 如何使用 CORS 代理绕过“没有 Access-Control-Allow-Origin 头部”
- 如何修复“Access-Control-Allow-Origin 头部不能是通配符”