15 張精美動圖全面講解 CORS

前言:

本文翻譯自 Lydia Hallie 小姐姐寫的 ✋🏼🔥 CS Visualized: CORS,她用了大量的動圖去解釋 CORS 這個概念,國內還沒有人翻譯本文,所以我在原文的理解上翻譯了本文並修改了一些錯誤,希望能幫到大家。

覺得翻譯的不錯一定要點贊哦,謝謝你,這對我真的很重要! 🌟

注:原文的動圖均爲 keynote 製作



前端開發中,我們經常要使用其他站點的數據。前端顯示這些數據之前,必須向服務器發出請求以獲取該數據。

假設我們正在訪問 https://api.mywebsite.com 這個站點,點擊按鈕向 https://api.mywebsite.com/users 發送請求,獲取網站上的一些用戶信息:

⚠️:這裏原作者有個筆誤,把 https://api.mywebsite.com 誤寫爲 https://www.mywebsite.com 了,圖中也有這個錯誤,讀者要注意一下不要被誤導

從結果上看錶現非常完美,我們向服務器發送請求,服務器返回了我們需要的 JSON 數據,前端也正常的渲染出了結果。

下面我們換一個網站試試。用 https://www.anotherwebsite.com 這個網站向 https://api.website.com/users 發送請求:

問題來了,我們請求同樣的接口網站,但是這次瀏覽器給我們拋出一個 Error。

剛剛瀏覽器拋出的就是 CORS Error,下面讓我們分析一下爲什麼會產生這種 Error,以及這個 Error 的確切含義是什麼。

1.同源策略

瀏覽器網絡請求時,有一個同源策略的機制。即默認情況下,使用 API 的 Web 應用程序只能從加載應用程序的同一個域請求 HTTP 資源。

比如說, https://www.mywebsite.com 請求 https://www.mywebsite.com/page 是完全沒有問題的。但是當資源位於不同協議子域端口的站點時,這個請求就是跨域的。

目前來看,同源策略會讓三種行爲受限:

  • Cookie、LocalStorage 和 IndexDB 訪問受限
  • 無法操作跨域 DOM(常見於 iframe)
  • Javascript 發起的 XHR 和 Fetch 請求受限

那麼,爲什麼會存在同源策略呢?

我們做個假設,如果不存在同源策略,你無意中點擊了七大姑在微信上給你發的一篇養生文章鏈接。其實這個網頁是個釣魚網站,訪問鏈接後就把你重定向到一個嵌入了 iframe 的攻擊網站,這個 iframe 會自動加載銀行網站,並通過 cookies 登錄你的賬戶。

登陸成功後,這個釣魚網站還可以控制 iframe 的 DOM,通過一系列騷操作把你卡里的錢轉走。

這是一個非常嚴重的安全漏洞,我們不希望自己在互聯網的內容被隨便訪問,更不要說這種涉及到錢的網站了。

同源策略可以幫助我們解決這個安全問題,這個策略確保我們只能訪問同一站點的資源。

在這種情況下,https://www.evilwebsite.com 嘗試跨站訪問 https://www.bank.com 的資源,同源策略就會阻止這個操作,讓釣魚網站無法訪問銀行網站的數據。

說了這麼多,同源策略和 CORS 又有什麼關係?

2.瀏覽器 CORS

出於安全原因,瀏覽器限制從腳本內發起的跨域 HTTP 請求。 例如 XHR 和 Fetch 就遵循同源策略。這意味着使用 API 的 Web 應用程序只能從加載應用程序的同一個域請求 HTTP 資源。

日常的業務開發中,我們會經常訪問跨域資源,爲了安全的請求跨域資源,瀏覽器使用一種稱爲 CORS 的機制。

CORS 的全名是 Cross-Origin Resource Sharing,即跨域資源共享。儘管默認情況下瀏覽器禁止我們訪問跨域資源,但是我們可以利用 CORS 放寬這種限制,在保證安全性的前提下訪問跨域資源。

瀏覽器可以利用 CORS 機制,放行符合規範的跨域訪問,阻止不合規範的跨域訪問。瀏覽器內部是怎麼做的呢?我們下面就來分析一下。

Web 程序發出跨域請求後,瀏覽器會自動向我們的 HTTP header 添加一個額外的請求頭字段:OriginOrigin 標記了請求的站點來源:

GET https://api.website.com/users HTTP/1/1
Origin: https://www.mywebsite.com // <- 瀏覽器自己加的

爲了使瀏覽器允許訪問跨域資源, 服務器返回的 response 還需要加一些響應頭字段,這些字段將顯式表明此服務器是否允許這個跨域請求。

3.服務端 CORS

作爲服務器開發人員,我們可以通過在 HTTP 響應中添加額外的響應頭字段 Access-Control-* 來表明是否允許跨域請求。根據這些 CORS 響應頭字段,瀏覽器可以允許一些被同源策略限制的跨源響應。

雖然有好幾個 CORS 響應頭字段,但有一個字段是必加的,那就是 Access-Control-Allow-Origin。這個頭字段的值指定了哪些站點被允許跨域訪問資源。

1️⃣ 如果我們有服務器的開發權限,我們可以給 https://www.mywebsite.com 加上訪問權限:將該域添加到 Access-Control-Allow-Origin 中。

這個響應頭字段現在被添加到服務器發回給客戶端的 response header 中。這個字段添加後,如果我們從 https://www.mywebsite.com 發送跨域請求,同源策略將不再限制 https://api.mywebsite.com 站點返回的資源。

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.mywebsite.com
Date: Fri, 11 Oct 2019 15:47 GM
Content-Length: 29
Content-Type: application/json
Server: Apache

{user: [{...}]}

2️⃣ 收到服務器返回的 response 後,瀏覽器中的 CORS 機制會檢查 Access-Control-Allow-Origin 的值是否等於 request 中 Origin 的值。

在這個例子中,request 的 Originhttps://www.mywebsite.com,這和 response 中 Access-Control-Allow-Origin 的值是一樣的:

3️⃣ 瀏覽器校驗通過,前端成功地接收到跨域資源。


那麼,當我們試圖從一個沒有在 Access-Control-Allow-Origin 中列出的網站跨域訪問這些資源會發生什麼呢?

如上圖所示,從 https://www.anotherwebsite.com 跨域訪問 https://api.mywebsite.com 資源,瀏覽器拋出一個 CORS Error,經過上面的講解,我們可以讀懂這個報錯信息了:

The 'Access-Control-Allow-Origin' header has a value
 'https://www.mywebsite.com' that is not equal 
to the supplied origin. 

在這種情況下,Origin 的值是 https://www.anotherwebsite.com。然而,服務器在 Access-Control-Allow-Origin 響應頭字段中沒有標記這個站點,瀏覽器 CORS 機制就阻止了這個響應,我們無法在我們的代碼中獲取響應數據。

CORS 還允許我們添加通配符 * 作爲允許的外域,這意味着該資源可以被任意外域訪問,所以要注意這種特殊情況


Access-Control-Allow-Origin 是 CORS 機制提供的衆多頭字段之一。服務器開發人員還可以通過其它頭字段擴展服務器的 CORS 策略,以允許/禁止某些請求。

另一個常見的響應頭字段是 Access-Control-Allow-Methods。其指明瞭跨域請求所允許使用的 HTTP 方法。

在上圖的案例中,只有GETPOSTPUT 方法被允許跨域訪問資源。其他 HTTP 方法,例如 PATCHDELETE 都會被阻止。

如果您想知道其它的 CORS 響應頭字段是什麼以及它們的用途,可以查看此列表

說到PUTPATCHDELETE 這幾個 HTTP 方法,CORS 處理這些方法時還有些不同。這些非簡單請求會觸發 CORS 的預檢請求。

4.預檢請求

CORS 有兩種類型的請求:一種是簡單請求(simple request),一種是預檢請求(preflight request)。一個跨域請求到底是簡單的的還是預檢的,取決於一些 request header。

當請求是 GETPOST 方法並且沒有任何自定義 Header 字段時,一般來說就是個簡單請求。除此之外的任何請求,諸如 PUTPATCHDELETE 方法,將會產生預檢。

如果你想知道一個請求必須滿足哪些要求才能成爲簡單請求,可以查看 MDN 簡單請求相關的文檔

說了這麼多,「預檢請求」到底是什麼意思?下面我們就來探討一下。


1️⃣ 在發送實際請求之前,客戶端會先使用 OPTIONS 方法發起一個預檢請求,預檢請求的 Access-Control-Request-* 中包含有關我們將要處理的實際請求的信息:

OPTIONS https://api.mywebsite.com/user/1 HTTP/1.1
Origin: https://www.mywebsite.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type

2️⃣ 服務器接收到預檢請求後,會返回一個沒有 body 的 HTTP 響應,這個響應標記了服務器允許的 HTTP 方法和 HTTP Header 字段:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://www.mywebsite.com
Access-Control-Request-Method: GET POST PUT
Access-Control-Request-Headers: Content-Type

3️⃣ 瀏覽器收到預檢響應,並檢查是否應允許發送實際請求。

⚠️:上圖預檢響應漏了 Access-Control-Allow-Headers: Content-Type

4️⃣ 如果預檢響應檢測通過,瀏覽器會將實際請求發送到服務器,然後服務器返回我們需要的資源。

如果預檢響應沒有檢驗通過,CORS 會阻止跨域訪問,實際的請求永遠不會被髮送。預檢請求是一種很好的方式,可以防止我們訪問或修改那些沒有啓用 CORS 策略的服務器上的資源。

💡 爲了減少網絡往返次數,我們可以通過在 CORS 請求中添加 Access-Control-Max-Age 頭字段來緩存預檢響應。瀏覽器可以使用緩存來代替發送新的預檢請求。

5.認證

XHR 或 Fetch 與 CORS 的一個有趣的特性是,我們可以基於 Cookies 和 HTTP 認證信息發送身份憑證。一般而言,對於跨域 XHR 或 Fetch 請求,瀏覽器不會發送身份憑證信息。

儘管 CORS 默認情況下不發送身份憑證,但我們可以通過添加 Access-Control-Allow-Credentials CORS 響應頭來更改它。

如果要在跨域請求中包含 cookie 和其他授權信息,我們需要做以下操作:

  • XHR 請求中將 withCredentials 字段設置爲 true
  • Fetch 請求中將 credentials 設爲 include
  • 服務器把 Access-Control-Allow-Credentials: true 添加到響應頭中
// 瀏覽器 fetch 請求
fetch('https://api.mywebsite,com.users', {
  credentials"include"
})

// 瀏覽器 XHR 請求
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;

// 服務器添加認證字段
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true

把上面的工作做好後,我們就可以在跨域請求中包含身份憑證信息了。

6.總結

CORS Error 一定程度上會讓前端開發很頭疼,但是遵循它的相關規定後,它可以讓我們在瀏覽器中進行安全的跨域請求。

同源策略和 CORS 的知識點有很多,本文只講了一些關鍵知識點,如果你想全面學習 CORS 的相關知識,我推薦你查閱MDN 文檔W3C 規範,這些一手知識是最準確的。

7.最後

這篇文章就到此結束了,如果覺得不錯的話一定要點贊鼓勵一下哦,祝大家學習進步,工作順利!

如果想要學習更多非筆記式的 HTTP 知識,可以看看我之前寫的舊文:

最後推薦一波我的個人號:滷蛋實驗室(egglabs),會更新一些前端技術與圖形學相關的文章,獨創不灌水,歡迎大家關注。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章