[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)是受限制的。
- 同源政策 @ MDN
跨來源資源共用(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) 問題:
這個問題會發生在當你的來源(origin),和你發送請求對象的來源是不同時。這是基於瀏覽器安全性考量所定義的規範,稱做同源政策(Same Origin Policy),也就是說程式碼所發出的跨來源 HTTP 請求(cross-origin HTTP Request)會受到限制。
基於安全性考量,程式碼所發出的跨來源 HTTP 請求會受到限制。例如,XMLHttpRequest
及 Fetch
都遵守同源政 策(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 攔下來的話,實際上請求已經發送出去,只是被瀏覽器擋下來不給你;但若是預檢請求,發送預檢請求後若沒有通過,真正要發送的請求是不會發送出去的,也就是說「一旦預檢請求完成,真正的請求才會被送出」。
預檢請求流程
用戶端向伺服器端發送預檢請求(瀏覽器會自動發送)
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
Access-Control-Request-Method
標頭會告訴伺服器之後送出的實際(actual)請求會是POST
方法。Access-Control-Request-Headers
標頭則是通知伺服器實際(actual)請求會帶有一個自定義的X-PINGOTHER
標頭。
伺服器回應預檢請求
在這些資訊下,接著伺服器將會確定是否接受請求,並傳送以下回覆:
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
標頭表示伺服器可以接受POST
、GET
和OPTIONS
方法。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 & Credentials
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")
參考
- 跨來源資源共用 @ MDN
- 同源政策
- 再也不學 AJAX 了!(三)跨域獲取資源 ② - JSONP & CORS @ SegmentFault
- 輕鬆理解 Ajax 與跨來源請求 @ Tech Bridge
- 原來 CORS 沒有我想像中的簡單 @ Tech Bridge