跨域資源共享 (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 表格僅在瀏覽器中載入

另請參閱