前言:
本文翻譯自 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 添加一個額外的請求頭字段:Origin
。Origin
標記了請求的站點來源:
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 的 Origin
是 https://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 方法。
在上圖的案例中,只有GET
,POST
或 PUT
方法被允許跨域訪問資源。其他 HTTP 方法,例如 PATCH
和 DELETE
都會被阻止。
“如果您想知道其它的 CORS 響應頭字段是什麼以及它們的用途,可以查看此列表。
說到PUT
,PATCH
和 DELETE
這幾個 HTTP 方法,CORS 處理這些方法時還有些不同。這些非簡單請求會觸發 CORS 的預檢請求。
4.預檢請求
CORS 有兩種類型的請求:一種是簡單請求(simple request),一種是預檢請求(preflight request)。一個跨域請求到底是簡單的的還是預檢的,取決於一些 request header。
當請求是 GET
或 POST
方法並且沒有任何自定義 Header 字段時,一般來說就是個簡單請求。除此之外的任何請求,諸如 PUT
,PATCH
或 DELETE
方法,將會產生預檢。
“如果你想知道一個請求必須滿足哪些要求才能成爲簡單請求,可以查看 MDN 簡單請求相關的文檔。
說了這麼多,「預檢請求」到底是什麼意思?下面我們就來探討一下。
1️⃣ 在發送實際請求之前,客戶端會先使用 OPTIONS
方法發起一個預檢請求,預檢請求的 Access-Control-Request-*
中包含有關我們將要處理的實際請求的信息:
首部字段 Access-Control-Request-Method
告知服務器,實際請求要用到的方法是什麼首部字段 Access-Control-Request-Headers
告知服務器,實際請求將附帶的自定義請求首部字段是什麼
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),會更新一些前端技術與圖形學相關的文章,獨創不灌水,歡迎大家關注。