不久前在公司寫了一個基於 Hapijs 的後端專案,感覺這個框架很有自己的特點,跟 Express 和 Koa 的區別比較大,體現了配置大於編碼的思想。用起來很方便,據說 Walmart 團隊用這個框架扛住了黑五的流量,看起來在實際專案中也有可用性,推薦大家嘗試一下~
有點跑題了,這篇文章主要寫我在開發過程中所遇到的一個問題,以及我從這個問題所學習到的東西,然後我是怎麼解決這個問題的。
一、問題
我的專案需求是寫一個 App 版本管理器,前後端都由我開發。前端分為兩個部分:運營人員寫版本更新說明的內部系統和 App 訪問的產品頁;後端就是對 App 版本進行管理的 CURD 介面。重點在於三個部分的程式部署在三臺伺服器上,前端的兩個系統在不同的伺服器對第三個伺服器上的介面進行資料請求,這就不可避免的涉及到了跨域。
當然,只是跨域的話也不難解決,新增 Access-Control-Allow-Origin
為要跨域的域名就 OK 了,或者直接賦值為 *
。但是我的部分介面涉及鑑權,通過 JWT 進行校驗,如果 JWT 不合法,那麼會返回 401 Unauthorized 錯誤;而我的 JWT 是通過請求頭的自定義欄位 authorization
帶到伺服器的,這就導致一個更加麻煩的問題出現了 —— 預檢請求。
二、收穫
什麼是預檢請求?
預檢請求(preflight request),是一個跨域請求,用來校驗當前跨域請求能否被理解。
它使用 HTTP 的 OPTIONS 請求,一般會包括一下請求頭:Access-Control-Request-Method
,Access-Control-Request-Headers
和 Origin
。
預檢請求通常在必要的時候由瀏覽器自動發起,不需要程式設計師進行干預。
如果我們想要知道伺服器是否支援一個 DELETE
請求,在傳送 DELETE
請求之前,伺服器通常會傳送一個如下的預檢請求:
OPTIONS /resource/foo
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://foo.bar.org
複製程式碼
如果伺服器允許使用 DELETE
方法的話,會返回如下響應頭;其中 Access-Control-Allow-Methods
會列出 DELETE
方法,代表伺服器支援這個方法。
HTTP/1.1 200 OK
Content-Length: 0
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400
複製程式碼
以上資料來源於 MDN
由此可知,預檢請求是一個用於校驗伺服器是否支援當前方法以及是否能夠理解當前請求的一種請求,它區別於一般的請求,不由程式碼發起,而在必要的時候由瀏覽器自動發出。
所以這裡就出問題了,如果我們不知道什麼時候瀏覽器會發出預檢請求,那麼伺服器沒有做處理的話就會導致 CORS 報錯的出現。
接下來再深入一點。
預檢請求與普通請求的區別
滿足以下條件的請求就是簡單請求:
-
一、請求方法屬於下面三種方法之一:
- HEAD
- POST
- GET
-
二、HTTP 的請求頭資訊超出一下範圍:
-
Accept
-
Accept-Language
-
Content-Language
-
Last-Event-ID
-
Content-Type:超出這三個的範圍:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
-
不滿足以上條件的請求就是非簡單請求。
如果是簡單的 CORS 請求,瀏覽器會自動在請求頭中新增一個 Origin 請求頭欄位,如果響應頭對應的 Access-Control-Allow-Origin
沒有包含 Origin 所指定的域,那麼就會報 CORS 錯誤,請求失敗。所以伺服器的響應要新增對應的響應頭。
如果是非簡單的 CORS 請求,那麼會有一次預檢請求,在正是請求之前發出一個 OPTIONS 請求對伺服器進行檢測。
除了有 Origin 以外,預檢請求的請求頭還包括一下兩個特殊欄位:
-
Access-Control-Request-Method
:表示 CORS 請求要用到的請求方法。 -
Access-Control-Request-Headers
:這是一個用逗號分割的字串,指出 CORS 請求要附加的請求頭。
伺服器的響應可以包含以下欄位:
-
Access-Control-Allow-Methods
:逗號分割的字串,表示允許的跨域請求方法。比如:
Access-Control-Allow-Methods: PUT, POST, GET, OPTIONS 複製程式碼
-
Access-Control-Allow-Headers
:如果瀏覽器請求包含Access-Control-Request-Headers
欄位,那麼伺服器中該響應頭也是必須的,也是一個由逗號分隔的字串,表示伺服器支援的請求頭。比如:
Access-Control-Allow-Headers: authorization 複製程式碼
-
Access-Control-Max-Age
:可選欄位,設定當前預檢請求的有效期,單位為秒。 -
Access-Control-Allow-Credentials
:可選欄位。預設情況下,CORS 請求不攜帶 cookie,如果伺服器想要 cookie,需要指定該請求頭為true
。
三、解決方法
-
避免出現預檢請求,需要使得你的請求滿足簡單請求的兩個條件。
比如在使用 JWT 鑑權時,可能會把你的 token 放在請求頭的 authorization 欄位,因為這個欄位超出了簡單請求的範圍,所以請求會變成非簡單請求。這時可以不把 token 放在 authorization 請求頭中。
-
出現預檢請求後,進行伺服器配置,分別設定好
Access-Control-Allow-Origin
、Access-Control-Allow-Methods
和Access-Control-Allow-Headers
,使得你的非簡單請求能夠通過預檢請求。 -
如果使用 Hapijs 的話,只需要在路由配置中增加
cors: true
配置即可。