GET /data/
Host: othersite.com
Origin: https://shubo.io
Access-Control-Allow-Origin
當 server 端收到這個跨來源請求時,它可以依據「請求的來源」,亦即 Origin 的值,決定是否要允許這個跨來源請求。如果 server 允許這個跨來源請求,它可以「授權」給這個來源的 JavaScript 存取這個資源。
授權的方法是在 response 裡加上 Access-Control-Allow-Origin header:
Access-Control-Allow-Origin: https://shubo.io
如果 server 允許任何來源的跨來源請求,那可以直接回 *:
Access-Control-Allow-Origin: *
當瀏覽器收到回應時,會檢查請求中的 Origin header 是否符合回應的 Access-Control-Allow-Origin header,相符的情況下瀏覽器就會讓這個請求成功,我們也可以順利地用 JavaScript 讀取到回應;反之,則瀏覽器會將這個 request 視為是不安全的而讓他失敗,即便 server 確實收到請求也成功地回應了,但基於安全性的理由 JavaScript 中沒有辦法讀到回應。
JavaScript 預設可以存取的「簡單」response header 有以下這些:
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
如果要讓 JavaScript 存取其他 header,server 端可以用 Access-Control-Expose-Headers header 設定。
X-MY-CUSTOM-HEADER: 123
X-MY-OTHER-CUSTOM-HEADER: 123
Access-Control-Expose-Headers: X-MY-CUSTOM-HEADER, X-MY-OTHER-CUSTOM-HEADER
一般跨來源請求
非「簡單」的跨來源請求,例如:HTTP PUT/DELETE 方法,或是 Content-Type: application/json 等,瀏覽器在發送請求之前會先發送一個 「preflight request(預檢請求)」,其作用在於先問伺服器:你是否允許這樣的請求?真的允許的話,我才會把請求完整地送過去。
Preflight Request (預檢請求)
什麼是 preflight request 呢?
Preflight request 是一個 http OPTIONS 方法,會帶有兩個 request header:Access-Control-Request-Method 和 Access-Control-Request-Headers。
Access-Control-Request-Method: 非「簡單」跨來源請求的 HTTP 方法。
Access-Control-Request-Headers 非「簡單」跨來源請求帶有的非「簡單」header。
比方說我發送的非「簡單」跨來源請求是這樣:
fetch ( 'https://othersite.com/data/' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'X-CUSTOM-HEADER' : '123'
那我的 request header 預計會長得會像這樣:
POST /data/
Host: othersite.com
Origin: https://shubo.io
Content-Type: application/json
X-MY-CUSTOM-HEADER: 123
瀏覽器幫我們發送的 preflight request 就會像這樣:
OPTIONS /data/
Host: othersite.com
Origin: https://shubo.io
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-MY-CUSTOM-HEADER, Content-Type
Preflight Response
那收到 preflight request 時,Server 該做什麼呢?
Server 必須告訴瀏覽器:我允許的方法和 header 有哪些。因此 Server 的回應必須帶有以下兩個 header:
Access-Control-Allow-Methods: 允許的 HTTP 方法。
Access-Control-Allow-Headers: 允許的非「簡單」header。
當瀏覽器看到跨來源請求的方法和 header 都有被列在允許的方法和 header 中,就表示可以實際發送請求了!
以上面提到例子來說,如果 server 可以接受上述的請求,server 的 preflight response 應該要像這樣:
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: X-MY-CUSTOM-HEADER, Content-Type
瀏覽器收到正確的 preflight response,表示 CORS 的驗證通過,就可以送出跨來源請求了!
接下來,瀏覽器實際幫我們送出以下的跨來源請求:
POST /data/
Host: othersite.com
Origin: https://shubo.io
Content-Type: application/json
X-MY-CUSTOM-HEADER: 123
最後一步,server 還是要回應 Access-Control-Allow-Origin header。瀏覽器會再檢查一次跨來源請求的回應是否帶有正確的 Access-Control-Allow-Origin header:
Access-Control-Allow-Origin: https://shubo.io
這一步也檢查無誤的話,我們的跨來源請求才算正式成功喔!這時候我們才能在 JavaScript 中讀取回應的內容:
fetch ( 'https://othersite.com/data/' , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'X-CUSTOM-HEADER' : '123'
. then ( response => response. json ( ) )
. then ( json => {
console. log ( json) ;
} ) ;
跨來源請求的 Cookie
一般的 http request 會帶有該網域底下的 cookie;然而,跨來源請求預設是不能帶 cookie 的。
為什麼呢?因為帶有 cookie 的請求非常強大,如果請求攜帶的 cookie 是 session token,那這個請求可以以你的身份做很多機敏的事情,像是存取你的隱私資料、從你的銀行帳戶轉帳等。
想了解 cookie 跨域相關的議題,可以參考以下文章:
延伸閱讀:[教學] Cookie 與 document.cookie
所以瀏覽器端針對跨來源請求的 cookie 也做了規範。
首先,請求必須要明確地標示「我要存取跨域 cookie」。使用 fetch API 和 XMLHttpRequest 的設定方法如下:
credentials
透過 fetch API 發送跨來源請求,需要設定 credentials: 'include':
fetch ( 'https://othersite.com/data' , {
credentials : 'include'
withCredentials
透過 XMLHttpRequest 發送跨來源請求,需要設定 withCredentials = true;
const xhr = new XMLHttpRequest ( ) ;
xhr. withCredentials = true ;
xhr. open ( 'POST' , 'https://othersite.com/data' ) ;
如此一來跨來源請求就會攜帶 cookie 了!
Server 端也需要額外的設定:如果是信任的來源,回應要帶有 Access-Control-Allow-Credentials header:
Access-Control-Allow-Credentials: true
如此一來,瀏覽器才會將 cookie 寫進該 domain。
注意:如果是允許使用 cookie 的情況,Access-Control-Allow-Origin 不能用 *,必須明確標示哪些來源允許存取。 理由也是基於安全性考量,因為可以用 cookie 的情況下,通常表示會存取一些比較個人化的資料,假設任何網站都能夠存取這樣的資料,顯然是有點危險的!所以不能設為 *!
如果你偷懶地用了 Access-Control-Allow-Origin: *,就會無情地收到來自瀏覽器的錯誤:
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when therequest's credentials mode is 'include'. Origin http://localhost:8080 is therefore not allowed access. Thecredentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
遇到 CORS 的問題,可以歸納出這樣的 SOP:
先認清楚是否為「簡單」的跨來源請求,如果是,在後端 GET/POST/HEAD 方法本身加上 Access-Control-Allow-Origin header。
如果非「簡單」跨來源請求,在後端 OPTIONS 加上 Access-Control-Allow-Methods 及 Access-Control-Allow-Headers header。另外,在後端方法本身加上 Access-Control-Allow-Origin header。
(Optional) 需要使用 cookie 的情況下,前端要加上 credentials: 'include' 或是 withCredentials 參數,後端要加上 Access-Control-Allow-Credentials header,而且 Access-Control-Allow-Origin header 不能用 *。