怎樣與 CORS 和 cookie 打交道

前端先鋒發表於2019-04-04

翻譯:瘋狂的技術宅

前言

CORS 與 cookie 在前端是個非常重要的問題,不過在大多數情況下,因為前後端的 domain 一般是相同的,所以很少去關心這些問題。或者只是要求後端設定 Access-Control-Allow-Origin: * 就行了,很少去了解背後運作的機制。

針對這個問題,MDN 上有非常詳細的解釋,所以這篇文章主要在於整理重點和實際操作時經常出現的問題。

同源策略(same-origin policy)

為了防止 javascript 在網頁上隨意撒野,同源策略規定了某些特定的資源,程式碼必須在同源的情況下才可以存取。

什麼是同源呢?一份 document 的來源,由 protocol、host 和 port 來定義。也就是說如果檔案1來自http://kalan.com,而檔案2來自於 https://kalan.com 他們就不算是同源。那如果是子域名呢?像是 https://api.foobar.comhttps://app.foobar.com。因為他們的 host 不同,所以也不算同一個源。

而有些資源是本來就能夠通過跨來源取得的:

  • <img />
  • <video /><audio />
  • <iframe />:可以通過定義 header 來防止他人嵌入
  • 通過 <link rel="stylesheet" href /> 載入的CSS指令碼
  • <script src="" /> 載入的 Javascript

通過程式碼發出的跨源請求則會受到同源策略的限制(如Fetch,XHR)。

很顯然,這樣的規定太過嚴格了。如果都要限制在同源策略下的話,前後端開發會難以進行,也沒辦法用 XHR 的方式套用其他 SDK 的 API。也因此出現了 CORS( Cross-Origin Resource Sharing)的機制。

CORS(跨源資源共享)

很多人都覺得 CORS 是前端才需要具備的知識。不過 CORS 通常需要後端設定相關的 HTTP 頭,並且瞭解背後的含義才有辦法正確運作。

那麼跨來源請求是怎麼運作的呢?

主要是由兩個 Header 來做相對的存取控制:請求當中的 Origin 和響應中的 Access-Control-Allow-Origin

只要傳送請求時的 Origin 和響應頭中 Access-Control-Allow-Origin 的值相同,或是 Access-Control-Allow-Origin: *(代表允許任何域存取資源),此時就會放寬 CORS 的限制,允許存取跨域資源。

如果不符合 CORS 策略的話,會顯示下列資訊:

img

如果你嘗試去讀取回傳的物件,還會得到警告。

首先,如果我們按照提示中所說的,將 fetch mode 改成 no-cors 會發生什麼事呢?的確,我們把煩人的錯誤資訊給處理掉了,但是情況似乎並沒有變好。

no-cors並不是靈丹妙藥,就算用了這個模式,CORS 也不會因此就開啟大門,也就是你的請求並不會成功發出。也因此出現了 SyntaxError: Unexpected end of input 這個錯誤。這個模式通常是跟Service Worker搭配使用的。

從上面這個實驗當中可知,要解除CORS的封印只有一招,就是在伺服器端加上正確的 Control-Access-Allow-Origin(host 必須跟原來相同或是*)。

另外,CORS 這個機制只會運作在 javascript 送出 XHR 或 fetch 時,一般 curl 或 postman 並沒有這個機制,所以也因此常常在測試 API 端點時會忽略這件事,導致前後端在測試 API 時發生出入。

有些跨來源請求不會發生 preflight,而有些請求則會,MDN上寫的清清楚楚:

  1. 必須是 GET,HEAD,POST 中的一種方法
  2. 除了 user-agent 自動設定的 header 和特定的 header 之外,不包含其他 header 。可接受的header
  3. 若有 Content-Type(注意是請求頭,不是響應頭),則必須是下列的值:application/x-www-form-encodedtext/plainmultipart/form-data

也就是說如果不滿足以上條件的話,就會發出 preflight 請求。

我們試著把 Content-Type 改為 application/json 來測試一下(不能為 application/x-www-form-encodedtext/plainmultipart/form-data)。

Preflight

所謂的 preflight 就是請求會先用 HTTP 的 OPTION 方法去另外一個域敲門,確認沒問題後才會送出真正的請求。一旦觸發了這個條件,事情就會變得麻煩得多。

  1. 必須加入一個 OPTIONS 的相同 api endpoint,並且設定 Access-Control-Allow-Origin 來符合 CORS 條件
  2. 必須加入Access-Control-Allow-Headers,且必須包含所有不在條件內 header,否則無法通過。

如果沒有通過 preflight check 的話,會得到錯誤資訊如下:

Access to fetch at 'http://localhost:3001/trigger-preflight' from origin 'http://localhost:3000' has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
複製程式碼

或是你沒有在 OPTIONS 的響應頭裡加上 Access-Control-Allow-Origin

Access to fetch at 'http://localhost:3001/trigger-preflight' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
複製程式碼

如果成功的話,你會看到 network 裡有兩個請求,一個是 OPTIONS,另一個則是真正的請求。

OPTION

GET

上圖為 OPTION,下圖為GET

如果我們加上一個自制的頭呢?根據MDN所定義的條件,也應該觸發預檢請求才對,我們加上一個X-Access-Token看看會發生什麼事。

怎樣與 CORS 和 cookie 打交道

的確無法通過preflight,如果要通過的話,必須再把 X-Access-Token 加入 Access-Control-Allow-Headers 中。

附帶身份驗證的請求

cookie 並不能跨域傳遞,也就是說不同 origin 來的 cookie 沒辦法互相傳遞及存取,不然就天下大亂了。

不過如果你在 a 域送出了 b 域的請求,且 b 域回傳了 cookie 的資訊,那麼在 a 域會以 b 域的形式儲存一份cookie,如果沒有設定 withCredentials 或是 credentials: ‘include’ 的話,就算伺服器回傳了 Set-Cookie,一樣不會被寫入。如下圖:

伺服器回傳Set-Cookie

伺服器回傳Set-Cookie

沒有寫入到瀏覽器中

沒有寫入瀏覽器中

在一般情況下如果再使用 b 域的 API,cookie 是不會自動被送出去的。這個情況下,你必須在 XHR 設定 withCredentials 或是 fetch 的選項中設定 { credentials: 'include' },因為這也是一個跨域請求,所以也必須按照 CORS 條件加入 Access-Control-Allow-Origin

為了避免安全性的問題,瀏覽器還有規定 Access-Control-Allow-Origin不能為*

Access to fetch at 'http://localhost:3001/cookie' from origin 'http://localhost:3000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
複製程式碼

不過僅僅這樣還是不夠,瀏覽器會自動拒絕沒有 Access-Control-Allow-Credentials 的響應,所以如果要將身份資訊傳到跨域的伺服器中,必須額外加上 Access-Control-Allow-Credentials: true。如果這些都設定成功,應該會像下圖這樣,在 Request Cookie可以看到 cookie 被成功送出。

Request Cookies 裡有個 jack!

Request Cookies 裡有個 jack!

好吧,如果你成功設定了這些東西,但還是有可能沒辦法把 cookie 送到伺服器。那有可能會是以下幾種情況:

1.使用者禁用了此域的 cookie

可能使用者把你加入了黑名單,導致 cookie 無法成功送出

解決方法:

  • 改域
  • 檢討自己為什麼被使用者封鎖
2.使用者阻止了所有外部網站的cookie

在Safari 中有時會開啟“阻止所有Cookie”這一選項,這在除錯時會讓你嚐到不少苦頭。

後記

要處理 CORS 是件吃力不討好的事情,尤其是有時在跑 CI/CD之前忘記加上 Access-Control-Allow-Origin 或是 Access-Control-Allow-Credentials,那麼部署可能又是一天以後的事了。這次把一些常見的問題整理起來,希望以後如果再有類似的情形可以知道怎麼處理。

最後附上原始碼

參考文章

歡迎關注京程一燈公眾號:京程一燈,獲取更多前端乾貨。

翻譯:瘋狂的技術宅

前言

CORS 與 cookie 在前端是個非常重要的問題,不過在大多數情況下,因為前後端的 domain 一般是相同的,所以很少去關心這些問題。或者只是要求後端設定 Access-Control-Allow-Origin: * 就行了,很少去了解背後運作的機制。

針對這個問題,MDN 上有非常詳細的解釋,所以這篇文章主要在於整理重點和實際操作時經常出現的問題。

同源策略(same-origin policy)

為了防止 javascript 在網頁上隨意撒野,同源策略規定了某些特定的資源,程式碼必須在同源的情況下才可以存取。

什麼是同源呢?一份 document 的來源,由 protocol、host 和 port 來定義。也就是說如果檔案1來自http://kalan.com,而檔案2來自於 https://kalan.com 他們就不算是同源。那如果是子域名呢?像是 https://api.foobar.comhttps://app.foobar.com。因為他們的 host 不同,所以也不算同一個源。

而有些資源是本來就能夠通過跨來源取得的:

  • <img />
  • <video /><audio />
  • <iframe />:可以通過定義 header 來防止他人嵌入
  • 通過 <link rel="stylesheet" href /> 載入的CSS指令碼
  • <script src="" /> 載入的 Javascript

通過程式碼發出的跨源請求則會受到同源策略的限制(如Fetch,XHR)。

很顯然,這樣的規定太過嚴格了。如果都要限制在同源策略下的話,前後端開發會難以進行,也沒辦法用 XHR 的方式套用其他 SDK 的 API。也因此出現了 CORS( Cross-Origin Resource Sharing)的機制。

CORS(跨源資源共享)

很多人都覺得 CORS 是前端才需要具備的知識。不過 CORS 通常需要後端設定相關的 HTTP 頭,並且瞭解背後的含義才有辦法正確運作。

那麼跨來源請求是怎麼運作的呢?

主要是由兩個 Header 來做相對的存取控制:請求當中的 Origin 和響應中的 Access-Control-Allow-Origin

只要傳送請求時的 Origin 和響應頭中 Access-Control-Allow-Origin 的值相同,或是 Access-Control-Allow-Origin: *(代表允許任何域存取資源),此時就會放寬 CORS 的限制,允許存取跨域資源。

如果不符合 CORS 策略的話,會顯示下列資訊:

img

如果你嘗試去讀取回傳的物件,還會得到警告。

首先,如果我們按照提示中所說的,將 fetch mode 改成 no-cors 會發生什麼事呢?的確,我們把煩人的錯誤資訊給處理掉了,但是情況似乎並沒有變好。

no-cors並不是靈丹妙藥,就算用了這個模式,CORS 也不會因此就開啟大門,也就是你的請求並不會成功發出。也因此出現了 SyntaxError: Unexpected end of input 這個錯誤。這個模式通常是跟Service Worker搭配使用的。

從上面這個實驗當中可知,要解除CORS的封印只有一招,就是在伺服器端加上正確的 Control-Access-Allow-Origin(host 必須跟原來相同或是*)。

另外,CORS 這個機制只會運作在 javascript 送出 XHR 或 fetch 時,一般 curl 或 postman 並沒有這個機制,所以也因此常常在測試 API 端點時會忽略這件事,導致前後端在測試 API 時發生出入。

有些跨來源請求不會發生 preflight,而有些請求則會,MDN上寫的清清楚楚:

  1. 必須是 GET,HEAD,POST 中的一種方法
  2. 除了 user-agent 自動設定的 header 和特定的 header 之外,不包含其他 header 。可接受的header
  3. 若有 Content-Type(注意是請求頭,不是響應頭),則必須是下列的值:application/x-www-form-encodedtext/plainmultipart/form-data

也就是說如果不滿足以上條件的話,就會發出 preflight 請求。

我們試著把 Content-Type 改為 application/json 來測試一下(不能為 application/x-www-form-encodedtext/plainmultipart/form-data)。

Preflight

所謂的 preflight 就是請求會先用 HTTP 的 OPTION 方法去另外一個域敲門,確認沒問題後才會送出真正的請求。一旦觸發了這個條件,事情就會變得麻煩得多。

  1. 必須加入一個 OPTIONS 的相同 api endpoint,並且設定 Access-Control-Allow-Origin 來符合 CORS 條件
  2. 必須加入Access-Control-Allow-Headers,且必須包含所有不在條件內 header,否則無法通過。

如果沒有通過 preflight check 的話,會得到錯誤資訊如下:

Access to fetch at 'http://localhost:3001/trigger-preflight' from origin 'http://localhost:3000' has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
複製程式碼

或是你沒有在 OPTIONS 的響應頭裡加上 Access-Control-Allow-Origin

Access to fetch at 'http://localhost:3001/trigger-preflight' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
複製程式碼

如果成功的話,你會看到 network 裡有兩個請求,一個是 OPTIONS,另一個則是真正的請求。

OPTION

GET

上圖為 OPTION,下圖為GET

如果我們加上一個自制的頭呢?根據MDN所定義的條件,也應該觸發預檢請求才對,我們加上一個X-Access-Token看看會發生什麼事。

怎樣與 CORS 和 cookie 打交道

的確無法通過preflight,如果要通過的話,必須再把 X-Access-Token 加入 Access-Control-Allow-Headers 中。

附帶身份驗證的請求

cookie 並不能跨域傳遞,也就是說不同 origin 來的 cookie 沒辦法互相傳遞及存取,不然就天下大亂了。

不過如果你在 a 域送出了 b 域的請求,且 b 域回傳了 cookie 的資訊,那麼在 a 域會以 b 域的形式儲存一份cookie,如果沒有設定 withCredentials 或是 credentials: ‘include’ 的話,就算伺服器回傳了 Set-Cookie,一樣不會被寫入。如下圖:

伺服器回傳Set-Cookie

伺服器回傳Set-Cookie

沒有寫入到瀏覽器中

沒有寫入瀏覽器中

在一般情況下如果再使用 b 域的 API,cookie 是不會自動被送出去的。這個情況下,你必須在 XHR 設定 withCredentials 或是 fetch 的選項中設定 { credentials: 'include' },因為這也是一個跨域請求,所以也必須按照 CORS 條件加入 Access-Control-Allow-Origin

為了避免安全性的問題,器瀏覽還有規定 Access-Control-Allow-Origin不能為*

Access to fetch at 'http://localhost:3001/cookie' from origin 'http://localhost:3000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
複製程式碼

不過僅僅這樣還是不夠,器瀏覽會自動拒絕沒有 Access-Control-Allow-Credentials 的響應,所以如果要將身份資訊傳到跨域的伺服器中,必須額外加上 Access-Control-Allow-Credentials: true。如果這些都設定成功,應該會像下圖這樣,在 Request Cookie可以看到 cookie 被成功送出。

Request Cookies 裡有個 jack!

Request Cookies 裡有個 jack!

好吧,如果你成功設定了這些東西,但還是有可能沒辦法把 cookie 送到伺服器。那有可能會是以下幾種情況:

1.使用者禁用了此域的 cookie

可能使用者把你加入了黑名單,導致 cookie 無法成功送出

解決方法:

  • 改域
  • 檢討自己為什麼被使用者封鎖
2.使用者阻止了所有外部網站的cookie

在Safari 中有時會開啟“阻止所有Cookie”這一選項,這在除錯時會讓你嚐到不少苦頭。

後記

要處理 CORS 是件吃力不討好的事情,尤其是有時在跑 CI/CD之前忘記加上 Access-Control-Allow-Origin 或是 Access-Control-Allow-Credentials,那麼部署可能又是一天以後的事了。這次把一些常見的問題整理起來,希望以後如果再有類似的情形可以知道怎麼處理。

最後附上原始碼

參考文章

歡迎關注京程一燈公眾號:jingchengyideng,獲取更多前端乾貨。

怎樣與 CORS 和 cookie 打交道

相關文章