原文: https://medium.com/@baphemot/understanding-cors-18ad6b478e2b
只要用過 AJAX,你應該就很熟悉瀏覽器控制檯中出現的如下報錯:
當你看到這個資訊,就意味著響應失敗了;但你依然能在瀏覽器開發工具的網路 tab 裡看到返回資料 -- 這是什麼情況呢?
跨域資源共享(CORS: Cross-Origin Resource Sharing)
你所觀察到的這種行為是瀏覽器 CORS 實現機制的效果。在 CORS 成為標準之前,由於安全原因,沒有辦法跨域呼叫 API。也就是(一定程度上依舊是)被所謂同源策略(Same-Origin Policy)限制住了。
CORS 機制是為了在認可使用者發起的請求的同時,阻止那些惡意 JS;並在以下情況發起的 HTTP 請求時被觸發:
- 一個不同的域(比如從 example.com 的站點呼叫 api.com)
- 一個不同的子域(比如從 example.com 的站點呼叫 api.example.com)
- 一個不同的埠(比如從 example.com 的站點呼叫 example.com:3001)
- 一個不同的協議(比如從 https://example.com 的站點呼叫 http://example.com)
這種機制阻止了當你已經登入 www.yourbank.com 的情況下,攻擊者在各種網站上植入的指令碼(比如通過 Google Ads 顯示的廣告)向 www.yourbank.com 發起的攜帶 你的身份憑證 的 AJAX 請求。
對於“簡單的” GET 或 POST 請求,如果伺服器沒有對其作出攜帶特殊 HTTP 頭部的響應 -- 請求依然被髮送並且資料也照樣被返回,但瀏覽器將不允許 Javascript 訪問該響應。
如果瀏覽器嘗試著去弄一個“沒那麼簡單”的請求(比如一個包含了 cookie 的請求,或 Content-type 不是 application/x-www-form-urlencoded
、multipart/form-data
、text-plain
三者之一的),則被稱為預檢(preflight)的機制將被用到,並且一個 OPTIONS 請求會被髮往伺服器。
關於“沒那麼簡單”的請求,一個常見的例子是在請求中加入 cookie 或自定義頭部 -- 如果瀏覽器傳送了這樣的請求且伺服器沒有正確響應的話,則只有預檢呼叫會傳送(不包含額外的頭部),而瀏覽器本應使用的真實的 HTTP 請求就不會被髮送了。
那些 Access-Control-Allow-
什麼什麼的...
在 CORS 請求和響應中,都用到了一些 HTTP 頭部,其中以下這幾個是你必須理解的:
Origin
該頭部是客戶端發起的請求的一部分,包含了應用所在的域。由於安全原因,瀏覽器不會允許使用者重寫這個值。
Access-Control-Allow-Credentials
該頭部只需要在伺服器支援通過 cookie 認證的情況下出現在響應中。這種情況下,其唯一合法值就是 true。
Access-Control-Allow-Methods
一個逗號分隔的、表示伺服器將會支援的 HTTP 請求動詞(如 GET, POST)列表。
Access-Control-Allow-Headers
格式為一個逗號分隔的列表,表示伺服器將會支援的請求頭部值。如果使用了自定義頭部(比如 x-authentication-token),則應該將其置於這個 ACA 頭部(譯註:即 Access-Control-Allow-Headers
)響應中,並返回到 OPTIONS 呼叫中;除非該請求被阻塞了。
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
複製程式碼
Access-Control-Expose-Headers
相似的是,該響應應包含一個頭部列表,表示將在實際的響應中出現的值,並應在客戶端中有效。所有其他頭部則會被限制。
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
複製程式碼
如何搞定 CORS “錯誤” ?
首先要清楚的是,CORS 行為並非一種錯誤 -- 這種機制致力於保護你的使用者、你本身,或你呼叫的站點。
有時,缺少合適的頭部,會導致客戶端的錯誤執行(如丟失了 API key 等認證資訊)。
取決於你面臨的場景,以下手段可以“搞定這種錯誤”:
A -- 我開發前端,也能控制後端,或者認識那個開發後端的哥們
這是最好的情況了 -- 你能根據呼叫,在伺服器上實現合適的 CORS 響應。如果 API 用 node 的 express 實現,那麼簡單的使用 cors
包(譯註:https://github.com/expressjs/cors)就可以了。如果要保證站點的適度安全,可以考慮為 Access-Control-Allow-Origin
設定一個白名單。
B -- 我開發前端,且暫時控制不了後端,我需要一個臨時的辦法
這是次優的情況,因為其實這就是手段 A,只是暫時性的受限。為了臨時解決,可以讓瀏覽器忽略 CORS 機制 -- 比如使用 ACAO Chrome 擴充套件
(譯註: 或指 Allow-Control-Allow-Origin: *
擴充套件) 或用如下引數在啟動 Chrome 時完全禁止 CORS:
chrome --disable-web-security --user-data-dir
複製程式碼
切記,這將禁止瀏覽器會話期間 所有 網站的 CORS 機制;要小心慎用。
另外的替代方法是使用 devServer.proxy
(假設你用到了 webpack 做開發);或使用一個 CORS-as-a-service
解決方案,比如 https://cors-anywhere.herokuapp.com/ 。
C -- 我開發前端,並總是控制不了後端的
Ok,現在事兒大了。首先要搞清為什麼伺服器沒有傳送適當的頭部。
也許是不允許第三方應用訪問其 API ?又或者其 API 只服務於伺服器端而非瀏覽器?要麼就是你需要在 URL 中傳送認證令牌?
如果你依然認為可以通過瀏覽器訪問資料,就得在瀏覽器應用和 API 之間編寫自己的代理了,就類似於我們在手段 B 中做的那樣。
該代理不必和你的應用執行在同樣的域下,只要當代理本身和客戶端通訊時正確支援 CORS 就行。代理和 API 之間的通訊就完全不必支援 CORS 了。
你既可以編寫自己的平臺,也可以使用諸如 https://www.npmjs.com/package/cors-anywhere 的成熟方案。
要記住如果你需要支援身份憑證,這樣的辦法會引入一個安全風險。
關於 CORS 的更多
如果希望學習更多關於 CORS 的細節,推薦閱覽這篇 MDN 文章 (https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)。
長按二維碼或搜尋 fewelife 關注我們哦