跨源資源共享(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

瀏覽器相容性

另見