翻譯:瘋狂的技術宅
前言
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.com
和 https://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 策略的話,會顯示下列資訊:
如果你嘗試去讀取回傳的物件,還會得到警告。
首先,如果我們按照提示中所說的,將 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上寫的清清楚楚:
- 必須是 GET,HEAD,POST 中的一種方法
- 除了 user-agent 自動設定的 header 和特定的 header 之外,不包含其他 header 。可接受的header
- 若有
Content-Type
(注意是請求頭,不是響應頭),則必須是下列的值:application/x-www-form-encoded
,text/plain
,multipart/form-data
也就是說如果不滿足以上條件的話,就會發出 preflight 請求。
我們試著把 Content-Type
改為 application/json
來測試一下(不能為 application/x-www-form-encoded
,text/plain
,multipart/form-data
)。
Preflight
所謂的 preflight 就是請求會先用 HTTP 的 OPTION 方法去另外一個域敲門,確認沒問題後才會送出真正的請求。一旦觸發了這個條件,事情就會變得麻煩得多。
- 必須加入一個 OPTIONS 的相同 api endpoint,並且設定 Access-Control-Allow-Origin 來符合 CORS 條件
- 必須加入
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
如果我們加上一個自制的頭呢?根據MDN所定義的條件,也應該觸發預檢請求才對,我們加上一個X-Access-Token
看看會發生什麼事。
的確無法通過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
沒有寫入瀏覽器中
在一般情況下如果再使用 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!
好吧,如果你成功設定了這些東西,但還是有可能沒辦法把 cookie 送到伺服器。那有可能會是以下幾種情況:
1.使用者禁用了此域的 cookie
可能使用者把你加入了黑名單,導致 cookie 無法成功送出
解決方法:
- 改域
檢討自己為什麼被使用者封鎖
2.使用者阻止了所有外部網站的cookie
在Safari 中有時會開啟“阻止所有Cookie”這一選項,這在除錯時會讓你嚐到不少苦頭。
後記
要處理 CORS 是件吃力不討好的事情,尤其是有時在跑 CI/CD之前忘記加上 Access-Control-Allow-Origin
或是 Access-Control-Allow-Credentials
,那麼部署可能又是一天以後的事了。這次把一些常見的問題整理起來,希望以後如果再有類似的情形可以知道怎麼處理。
最後附上原始碼。