与 简单请求 不同,对于“预检”请求,浏览器首先使用 OPTIONS 方法向另一个源上的资源发送 HTTP 请求,以确定是否可以安全发送实际请求。这种跨域请求是预检的,因为它们可能对用户数据有影响。
以下是一个将被预检的请求示例:
jsconst fetchPromise = fetch("https://bar.other/doc", {
method: "POST",
mode: "cors",
headers: {
"Content-Type": "text/xml",
"X-PINGOTHER": "pingpong",
},
body: "
});
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请求中需要。
让我们看一下客户端和服务器之间完整的交互。第一次交互是预检请求/响应。
httpOPTIONS /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 请求之外,还会发送另外两个请求标头。
httpAccess-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)是可以接受的。让我们更仔细地看一下以下几行。
httpAccess-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超过此值时,该值优先。
预检请求完成后,会发送实际请求。
httpPOST /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
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标头的情况下触发预检的请求,则无法使用上述步骤来解决此限制。除非您控制发出请求的服务器,否则您根本无法解决此限制。