CORS 解決方案:前端如何處理收不到 Response Header 的問題
最近我設計了一個 OAuth Token 驗證給前端串接,但發現前端無法抓取到我後端的 Response Header。經過一番研究,我發現問題出在 CORS 上。以前我對 CORS 都是簡單了解,沒想到 CORS 規範的內容如此豐富。這篇文章就是我對此進行深入研究的記錄。這篇還一點債了…
心智圖
因為 mermaid 無法用-
,所以用_
置換。
Origin Header
|
|
常見我們 AJAX 跨域會看到 HTTP 有 Origin
Header 內容。
從 MDN 我們看到:
Origin 標頭與 Referer 標頭類似,但前者不會暴露 URL 的 path 部分,而且其可以為 null 值。其用於為源站的請求提供 “安全上下文”,除非源站的資訊敏感或不必要的。
在深入研究之前,我們需要先了解 Origin 的規則。對這些規則有基本的理解,將有助於我們更好地理解後續的內容。我之前在 程式狂想筆記 中有寫過一篇關於 SameSite Cookie 和 Same Origin Policy 的文章,您可以參考。io/blog/posts/SameSite-Cookie-%E5%92%8C-Same-Origin-Policy-%E6%98%AF%E4%BB%80%E9%BA%BC/)有寫這篇筆記有記錄到。
我們經常會看到 Same-Origin Policy
,也就是所謂的 同源政策
。雖然這個詞彙中包含了 Origin
,但它與 CORS 的意義並不相同。MDN 上有一篇關於同源政策和 CORS 的文章,我們可以參考一下 same-origin_policy_and_cors。
簡單來說,Same-Origin Policy
是 Web 的一種基本安全機制,它限制了一個源 (origin) 的文件或腳本與來自另一個源的資源的交互方式。這種政策有助於隔離潛在的惡意文件,從而減少可能的攻擊。如果您想了解更多,可以參考 MDN 上的 Same-origin policy。
然而,在實際操作中,有些站點之間的交互是正常的,我們需要透過 CORS 來解除這種限制。
opaque origin
從這篇忍術!把 same site 變 same origin 之術! - Huli’s blog 看到裡面提到 opaque origin
,在開啟 file://xxx.html
做跨域請求, Origin
Header 會顯示 null
。
接著規範裡把 origin 分成兩種,一種是 An opaque origin,另一種是 A tuple origin。
Opaque origin 可以想成是在特殊狀況下才會出現的 origin,例如說我在本機開啟一個網頁,網址會是 file:///…,這時候在網頁內發送 request,origin 就會是 opaque origin,也就是 null。
Tuple origin 則是比較常見而且我們也比較關心的 origin
我這邊就做個簡單實驗,隨便新增一個 xxx.html
用瀏覽器打開,Console 打個 fetch('https://tw.yahoo.com')
。這時候就可以看到你的 header 有這個東西。
要嘛兩個是一樣的 opaque origin,否則的話要 scheme、host 跟 port 三者都相等,才是 same origin。除了 same origin 以外,你還會在 spec 裡面看到另外一個詞叫做「same origin-domain」,這個我們之後也會提到。
我這邊有自己實驗 file:///C:/Users/steve/Desktop/test.htm
做 AJAXfile:///C:/Users/steve/Desktop/test2.htm
,沒版法取得資料,就如上訴原因。
請求預檢
我們在使用 Ajax 進行跨域操作時,常常會看到 OPTIONS
請求。這實際上是一種 預檢
的動作(我們稍後會進一步解釋這個概念)。
過去,我們可能會誤以為只要網域不同就會進行跨域操作,但實際上規則更為細緻。**簡單請求不會進行預檢,而非簡單請求則會進行預檢。**接下來,我們將進一步了解什麼是簡單請求和非簡單請求。
|
|
CORS 種類
我們在進行跨域操作時,有時會看到 OPTIONS
請求,有時則不會。最近我讀到一篇文章,提到非簡單請求會進行 請求預檢
(preflight request)。我才知道有這種詳細機制。
CORS 簡單請求
一般為 Get
,Post
,Head
方法和 Content-Type
為傳統表單發送不會做預檢。但是還是會發送到 Server,而瀏覽器會判斷 Response Access-Control-Allow-Origin
有沒有這個 Header,沒有的話瀏覽器會阻擋程式成功這個請求,程式無法讀到 Response 內容,會報一個錯誤。
這邊我說的傳統表單 Content-Type
為 application/x-www-form-urlencoded
、multipart/form-data
或是 text/plain
。這邊只列出一點規則,詳細可以看這邊。
- 只使用了下面的安全首部字段,不得人為設定其他首部字段
Accept
Accept-Language
Content-Language
Content-Type僅限以下透明度
text/plain
multipart/form-data
application/x-www-form-urlencoded - HTML頭部欄位欄位:DPR、Download、Save-Data、Viewport-Width、WIdth
- 請求中的各個XMLHttpRequestUpload物件均沒有註冊任何事件監聽器;XMLHttpRequestUpload 物件可以使用 XMLHttpRequest.upload 屬性存取
- 請求中沒有使用 ReadableStream 對象
Access-Control-Allow-Origin
,但有時候,僅僅加入這個標頭並不能完全解決 CORS
問題。在這種情況下,這邊需要繼續看下去。POST
送出跨域我們知道不會被瀏覽器阻擋。但其實他在 Header 也有加上 Origin
,Form 表單送出 GET
方法就沒有這個東西。可參考: CORS 完全手冊(四):一起看規範 - Huli’s blog
CORS 非簡單請求
當我們使用 AJAX 進行 POST
請求,並且 Content-Type
設為 Application/Json
時,這種請求會被視為非簡單請求。如果請求中包含了非常見的Header),則該請求會被瀏覽器阻擋。(這裡我們先不詳細討論 Header)
一般 AJAX 做
POST
時候,Content-Type 都會帶Application/Json
我在這裡並沒有詳細說明何時會被視為非簡單請求,因為這個主題相當廣泛,且初學者可能會覺得難以理解。實際上,只有當請求方法為 GET
、HEAD
或 POST
,且標頭符合 CORS-safelisted request-header
時,該請求才會被視為簡單請求。
在上述範例中,除了我們在簡單查詢中需要帶的標頭之外,我們還看到在進行 請求預檢
時,會多帶 Access-Control-Request-Method
和 Access-Control-Request-Headers
兩個標頭。這兩個標頭在預檢請求的回應中,會對應到 Access-Control-Allow-Methods
和 Access-Control-Allow-Headers
。預檢請求完成後,瀏覽器會進行與簡單請求相同的步驟。
Access-Control-Allow-Origin
標頭。無論 Request 方法是 OPTIONS 還是 POST,這兩種請求都需要包含 Access-Control-Allow-Origin
標頭。Request 不能隨意帶 Header
在上述情況中,我們的 Request 符合 CORS-safelisted request-header
,因此不需要進行請求預檢
。讓我們進行一個簡單的實驗來驗證這一點:首先,打開這個網頁 https://web.cyut.edu.tw/
,然後在 Console 中輸入以下指令。
|
|
然而,如果我們在請求中加入任意的標頭,就會觸發請求預檢
。
|
|
上面簡單範例是自訂 header 會做請求預檢
,但實際上不是自訂才會。是不符合 CORS-safelisted request-header
才會,API 常用帶 Token 在 Header 上,會觸發請求預檢。
|
|
有哪些符合可以看 CORS-safelisted request header - MDN Web Docs Glossary: Definitions of Web-related terms | MDN。
如果你手動進行嘗試,你會發現 請求預檢
中多了 Access-Control-Request-Headers
,其中包含了你的程式中帶的標頭。這些都是瀏覽器自動為你處理的。當後端回應 Access-Control-Allow-Headers
時,表示你可以發送 API 請求。此時,你會發現沒有任何錯誤,這表示問題已經被解決了。
然而,如果你設計了讓前端抓取 Response Header,當後端回應 Access-Control-Allow-Headers
時,你可能會發現前端無法抓取到 Response Header,即使你已經設定了相關的 CORS 設定,且程式並未出現錯誤。這是因為瀏覽器並未將相關 Header 傳給前端。
Access-Control-Request-Headers
可以放多個值,他是用,
分開的。但其實你不需要管怎麼放,畢竟是瀏覽器幫你做到的。JS 不能隨意用 Response Header
上面章節有提到,AJAX 通了,假如你有設計 Response 給前端抓 Header,你會發現沒辦法抓到 Header 參數。解決方法就是在後端 API Response 加上 Access-Control-Expose-Headers
Header,就可以解決這個問題。
Access-Control-Expose-Headers
通常會帶完整參數,很多框架實作上沒有加上 *
,都是會回傳多個值 (header1,header2),但我測試用 *
,Response 就能吃到 Header 值。
|
|
前端 Header 可以抓到值了。
Access-Control-Allow-Credentials
這個設定是用於跨域分享 Cookies。在使用 fetch 時可能會用到。理論上,現在的 API 通常不會使用 Cookies,所以通常不會設定這個值,除非你的服務有 Web 頁面使用到 Session 或 Cookies。
Access-Control-Allow-Methods
這個設定允許前端呼叫的請求方法。這部分相對直觀,不再詳細說明。
Access-Control-Max-Age
CORS-preflight request 也是 CORS request 的一種,所以上面所說的針對 CORS request 可以給的 response 也都可以給。
而除此之外還定義了另外三個:
1 2 3
Access-Control-Allow-Methods:可以使用哪些 method Access-Control-Allow-Headers:可以使用哪些 header Access-Control-Max-Age:前兩個 header 可以快取多久
這邊值得注意的是第三個,預設值是 5 秒,所以 5 秒內針對同一個資源的 CORS response header 是可以重用的。
引用: CORS 完全手冊(四):一起看規範 - Huli’s blog
你真的會懂 CORS?
我們通常在網路上學習如何進行跨域設定,你可能會這樣設定:
|
|
在寫這篇文章時,我對後端程式框架設定 CORS 的理解並不深入,但讀了以下的資料後,我對 CORS 有了更深的理解:
|
|
我相信還有很多內容可以學習,以下是我推薦的閱讀資料:
- CORS 完全手冊(一):為什麼會發生 CORS 錯誤? - Huli’s blog Google Cache Page 備份
這篇文章中的兩個問題非常有趣,讓我對 CORS 有了新的認識。 - 跨域资源共享 CORS 详解 - 阮一峰的网络日志
- 同源政策 (Same Origin Policy, SOP)、內容安全政策(Content Security Policy, CSP)與跨來源資源共用 (Cross-Origin Resource Sharing, CORS) | Math.py
如果你有一段時間沒有接觸前端相關名詞,這篇文章的介紹非常清楚。
相關文章
-
关于 websocket 跨域的一个奇怪问题… - Java 技术栈 - 博客园
裡面提到 websocket 需要 CORS,看起來好像不用? -
ASP.NET Core Cross-Origin Resource Sharing (CORS):叡揚部落格:叡揚資訊
IIS 設定 CORS 方法。