怎樣與 CORS 和 cookie 打交道

翻譯:瘋狂的技術宅

本文首發微信公衆號:jingchengyideng
歡迎關注,天天都給你推送新鮮的前端技術文章javascript


前言

CORS 與 cookie 在前端是個很是重要的問題,不過在大多數狀況下,由於先後端的 domain 通常是相同的,因此不多去關心這些問題。或者只是要求後端設置 Access-Control-Allow-Origin: * 就好了,不多去了解背後運做的機制。前端

針對這個問題,MDN 上有很是詳細的解釋,因此這篇文章主要在於整理重點和實際操做時常常出現的問題。java

同源策略(same-origin policy)

爲了防止 javascript 在網頁上隨意撒野,同源策略規定了某些特定的資源,代碼必須在同源的狀況下才能夠存取。git

什麼是同源呢?一份 document 的來源,由 protocol、host 和 port 來定義。也就是說若是文件1來自http://kalan.com,而文件2來自於 https://kalan.com 他們就不算是同源。那若是是子域名呢?像是 https://api.foobar.comhttps://app.foobar.com。由於他們的 host 不一樣,因此也不算同一個源。程序員

而有些資源是原本就可以經過跨來源取得的:github

  • <img />
  • <video /><audio />
  • <iframe />:能夠經過定義 header 來防止他人嵌入
  • 經過 <link rel="stylesheet" href /> 載入的CSS腳本
  • <script src="" /> 載入的 Javascript

經過代碼發出的跨源請求則會受到同源策略的限制(如Fetch,XHR)。面試

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

CORS(跨源資源共享)

不少人都以爲 CORS 是前端才須要具有的知識。不過 CORS 一般須要後端設定相關的 HTTP 頭,而且瞭解背後的含義纔有辦法正確運做。segmentfault

那麼跨來源請求是怎麼運做的呢?後端

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

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

若是不符合 CORS 策略的話,會顯示下列信息:

clipboard.png

若是你嘗試去讀取回傳的物件,還會獲得警告。

首先,若是咱們按照提示中所說的,將 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,另外一個則是真正的請求。

clipboard.png

clipboard.png

上圖爲 OPTION,下圖爲GET

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

clipboard.png

的確沒法經過preflight,若是要經過的話,必須再把 X-Access-Token 加入 Access-Control-Allow-Headers 中。

附帶身份驗證的請求

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

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

clipboard.png

服務器回傳Set-Cookie

clipboard.png

沒有寫入瀏覽器中

在通常狀況下若是再使用 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 被成功送出。

clipboard.png

Request Cookies 裏有個 jack!

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

1.用戶禁用了此域的 cookie

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

解決方法:

  • 改域
  • 反省本身爲何被用戶封鎖
2.用戶阻止了全部外部網站的cookie

在Safari 中有時會開啓「阻止全部Cookie」這一選項,這在調試時會讓你嚐到很多苦頭。

後記

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

最後附上源代碼

參考文章


本文首發微信公衆號:jingchengyideng

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章


歡迎繼續閱讀本專欄其它高贊文章:

相關文章
相關標籤/搜索