跳至主要内容

[Web] 同源政策與跨來源資源共用(CORS)

keywords: CORS, Cross-Origin Resource Sharing, 同源策略, Same-origin policy

👍 CS Visualized: CORS @ dev

TL;DR;

  • 預設的情況下,瀏覽器同源會遵守同源政策(same-origin policy),並不允許跨域請求(cross-origin)。在 server 未允許的情況下,會得到 blocked by CORS policy 的錯誤。
  • 如果 client 希望能過取得跨域的資源,需要由 server 在 response header 中帶上 Access-Control-Allow-Origin 的欄位。

同源政策(same-origin policy)

在同源政策(same-origin policy)中規範了那寫資源可以跨源存取,哪些會受到限制。

同源的定義簡單如下:

  • 不同網域(Domain)
  • 不同通訊協定:HTTP, HTTPS, FTP
  • 不同連接埠號(Port)

一般來說跨來源寫(Cross-origin writes)、跨來源嵌入(Cross-origin embedding)是被允許的,而跨來源讀取(Cross-origin reads)是受限制的。

跨來源資源共用(cross-origin-resource-sharing, CORS)

fetch('https://www.google.com')
.then(function (response) {
// Do something ...
})
.catch(function (error) {
// Do something else ...
});

這時候會出現經典的 Cross-Origin Resource Sharing(CORS) 問題:

Imgur

這個問題會發生在當你的來源(origin),和你發送請求對象的來源是不同時。這是基於瀏覽器安全性考量所定義的規範,稱做同源政策(Same Origin Policy),也就是說程式碼所發出的跨來源 HTTP 請求(cross-origin HTTP Request)會受到限制。

基於安全性考量,程式碼所發出的跨來源 HTTP 請求會受到限制。例如,XMLHttpRequestFetch 都遵守同源政策(same-origin policy)。這代表網路應用程式所使用的 API 除非有回傳 CORS 標頭(例如,Access-Control-Allow-Origin),否則只能請求與應用程式相同來源的 HTTP 資源。

在這個情況下,其實請求(request)已經發出去了,而瀏覽器其實也拿到回應(response),但是瀏覽器基於同源政策,因此不把拿到的回應給你的 JavaScript 去做進一步的處理。

開啟跨來源請求

若要開啟跨來源請求,必須在伺服器端做一些設定,像是在 Response Header 加上 Access-Control-Allow-Origin

Access-Control-Allow-Origin: *                    # 允許所有網站發送的請求
Access-Control-Allow-Origin: http://foo.example # 只允許 http://foo.example 的請求

這裡 * 就表示接受所有不同來源的跨域請求,因此當瀏覽器接受到伺服器的回應時,會去比對這個內容,如果目前的 Origin 符合 Access-Control-Allow-Origin 所定義的規則的話,瀏覽器才會把回應給你。

另外還有其他和跨域請求時可用的 Header,像是:

Access-Control-Allow-Headers:
Access-Control-Allow-Methods: ["GET", "POST", "PUT"]
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
Access-Control-Max-Age: <delta-seconds>
Access-Control-Allow-Credentials: true
  • Access-Control-Allow-Headers:指定哪些 HTTP 標頭可以於實際請求中使用。
  • Access-Control-Allow-Methods:存取資源所允許的方法,用來回應預檢請求。
  • Access-Control-Expose-Headers:瀏覽器能夠存取伺服器回應當中哪些標頭。
  • Access-Control-Max-Age:預檢請求之結果可以被快取的秒數。
  • Access-Control-Allow-Credentials:用於驗證請求中。

簡單請求與預檢請求

跨來源請求又可以分成「簡單請求(simple request)」和「預檢請求(preflight request)」。

簡單請求(Simple Request)

簡單請求有一些規範,基本上使用的一定要是 GET、HEAD、POST 方法,並且不能有客制的 header,僅允許特定的標頭和內容,如此才算是簡單請求。

簡單請求(Simple Request) @ MDN > CORS

備註

雖然稱作是簡單請求,但它依然會遵循「同源政策(Same Origin Policy)」的規範。

預檢請求(Preflight Request)

預檢請求通常是在發送會帶有副作用的 HTTP 請求方法前,規範瀏覽器要先發送預檢請求,預檢請求會以 HTTP OPTIONS 的方法送出,以向伺服器確認後續的請求能否傳送,如果預檢請求沒有通過,那麼後續真正要發送的實際請求(例如,POST, PUT, DELETE 等等)就不會發送了。

預檢請求和簡單請求有一點不同,如果是簡單請求而被 CORS 攔下來的話,實際上請求已經發送出去,只是被瀏覽器擋下來不給你;但若是預檢請求,發送預檢請求後若沒有通過,真正要發送的請求是不會發送出去的,也就是說「一旦預檢請求完成,真正的請求才會被送出」。

預檢請求流程

imgur

用戶端向伺服器端發送預檢請求(瀏覽器會自動發送)
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
伺服器回應預檢請求

在這些資訊下,接著伺服器將會確定是否接受請求,並傳送以下回覆:

HTTP/1.1 200 OK
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://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-Methods 標頭表示伺服器可以接受 POSTGETOPTIONS 方法。
  • Access-Control-Allow-Headers 標頭及其值「X-PINGOTHER, Content-Type」,表示伺服器允許在實際(actual)請求中使用以上這兩個標頭。
  • Access-Control-Max-Age 提供了本次預檢請求回應所可以快取的秒數,如此將可避免瀏覽器不斷發送 preflight request。在此範例中,86400 秒即為 24 小時。請留意每一個瀏覽器都有預設的最大值,當 Access-Control-Max-Age 較預設值大時會優先採用預設值。

預檢請求(Preflight Request) @ MDN > CORS

Cookie、Authorization Header 和 TLS 的憑證,預設也只會自動帶入到同源所發出的請求中。

有時我們的 Cookie 會需要發送到不同來源(origin)的位置,例如 server 的 URL 是 https://api.example.com,但 client 的 URL 是 https://dev.example.com,這時候因為 subdomain 不同的緣故,會被認定是不同的 origin,因此 client 對 server 所發送的請求並不會自動帶上 Cookie。

要解決這個問題,如果 client 使用的是 fetch API,則需要加上 credentials: include; 的 options,否則 fetch 預設不會傳送 cross-origin cookies:

// client
fetch('https://api.example.com', {
credentials: 'include',
});

而 server 則是要回傳 Access-Control-Allow-Credentials 的 header 在 response 中:

headers.Set("Access-Control-Allow-Credentials", "true")

參考