15 張精美動圖全面講解 CORS

前言:
本文翻譯自 Lydia Hallie[1] 小姐姐寫的 ??? CS Visualized: CORS[2],她用了大量的動圖去解釋 CORS 這個概念,國內(nèi)還沒有人翻譯本文,所以我在原文的理解上翻譯了本文并修改了一些錯誤,希望能幫到大家。
覺得翻譯的不錯一定要點贊哦,謝謝你,這對我真的很重要!?
“注:原文的動圖均為 keynote 制作
前端開發(fā)中,我們經(jīng)常要使用其他站點的數(shù)據(jù)。前端顯示這些數(shù)據(jù)之前,必須向服務器發(fā)出請求以獲取該數(shù)據(jù)。
假設我們正在訪問 https://api.mywebsite.com 這個站點,點擊按鈕向 ?https://api.mywebsite.com/users 發(fā)送請求,獲取網(wǎng)站上的一些用戶信息:

“??:這里原作者有個筆誤,把
https://api.mywebsite.com誤寫為https://www.mywebsite.com了,圖中也有這個錯誤,讀者要注意一下不要被誤導
從結(jié)果上看表現(xiàn)非常完美,我們向服務器發(fā)送請求,服務器返回了我們需要的 JSON 數(shù)據(jù),前端也正常的渲染出了結(jié)果。
下面我們換一個網(wǎng)站試試。用 ?https://www.anotherwebsite.com 這個網(wǎng)站向 https://api.website.com/users 發(fā)送請求:

問題來了,我們請求同樣的接口網(wǎng)站,但是這次瀏覽器給我們拋出一個 Error。
剛剛瀏覽器拋出的就是 CORS Error,下面讓我們分析一下為什么會產(chǎn)生這種 Error,以及這個 Error 的確切含義是什么。
1.同源策略
瀏覽器網(wǎng)絡請求時,有一個同源策略的機制。即默認情況下,使用 API 的 Web 應用程序只能從加載應用程序的同一個域請求 HTTP 資源。
比如說, https://www.mywebsite.com 請求 ?https://www.mywebsite.com/page 是完全沒有問題的。但是當資源位于不同協(xié)議、子域或端口的站點時,這個請求就是跨域的。

目前來看,同源策略會讓三種行為受限:
Cookie、LocalStorage 和 IndexDB 訪問受限 無法操作跨域 DOM(常見于 iframe) Javascript 發(fā)起的 XHR 和 Fetch 請求受限
那么,為什么會存在同源策略呢?
我們做個假設,如果不存在同源策略,你無意中點擊了七大姑在微信上給你發(fā)的一篇養(yǎng)生文章鏈接。其實這個網(wǎng)頁是個釣魚網(wǎng)站,訪問鏈接后就把你重定向到一個嵌入了 iframe 的攻擊網(wǎng)站,這個 iframe 會自動加載銀行網(wǎng)站,并通過 cookies 登錄你的賬戶。
登陸成功后,這個釣魚網(wǎng)站還可以控制 iframe 的 DOM,通過一系列騷操作把你卡里的錢轉(zhuǎn)走。

這是一個非常嚴重的安全漏洞,我們不希望自己在互聯(lián)網(wǎng)的內(nèi)容被隨便訪問,更不要說這種涉及到錢的網(wǎng)站了。
同源策略可以幫助我們解決這個安全問題,這個策略確保我們只能訪問同一站點的資源。

在這種情況下,https://www.evilwebsite.com 嘗試跨站訪問 https://www.bank.com 的資源,同源策略就會阻止這個操作,讓釣魚網(wǎng)站無法訪問銀行網(wǎng)站的數(shù)據(jù)。
說了這么多,同源策略和 CORS 又有什么關系?
2.瀏覽器 CORS
出于安全原因,瀏覽器限制從腳本內(nèi)發(fā)起的跨域 HTTP 請求。例如 XHR 和 Fetch 就遵循同源策略。這意味著使用 API 的 Web 應用程序只能從加載應用程序的同一個域請求 HTTP 資源。

日常的業(yè)務開發(fā)中,我們會經(jīng)常訪問跨域資源,為了安全的請求跨域資源,瀏覽器使用一種稱為 CORS 的機制。
CORS 的全名是 Cross-Origin Resource Sharing,即跨域資源共享。盡管默認情況下瀏覽器禁止我們訪問跨域資源,但是我們可以利用 CORS 放寬這種限制,在保證安全性的前提下訪問跨域資源。
瀏覽器可以利用 CORS 機制,放行符合規(guī)范的跨域訪問,阻止不合規(guī)范的跨域訪問。瀏覽器內(nèi)部是怎么做的呢?我們下面就來分析一下。
Web 程序發(fā)出跨域請求后,瀏覽器會自動向我們的 HTTP header 添加一個額外的請求頭字段:Origin。Origin 標記了請求的站點來源:
GET https://api.website.com/users HTTP/1/1
Origin: https://www.mywebsite.com // <- 瀏覽器自己加的

為了使瀏覽器允許訪問跨域資源, 服務器返回的 response 還需要加一些響應頭字段,這些字段將顯式表明此服務器是否允許這個跨域請求。
3.服務端 CORS
作為服務器開發(fā)人員,我們可以通過在 HTTP 響應中添加額外的響應頭字段 Access-Control-* 來表明是否允許跨域請求。根據(jù)這些 CORS 響應頭字段,瀏覽器可以允許一些被同源策略限制的跨源響應。
雖然有好幾個 CORS 響應頭字段[3],但有一個字段是必加的,那就是 Access-Control-Allow-Origin。這個頭字段的值指定了哪些站點被允許跨域訪問資源。
1?? 如果我們有服務器的開發(fā)權限,我們可以給 https://www.mywebsite.com 加上訪問權限:將該域添加到 Access-Control-Allow-Origin 中。

這個響應頭字段現(xiàn)在被添加到服務器發(fā)回給客戶端的 response header 中。這個字段添加后,如果我們從 https://www.mywebsite.com 發(fā)送跨域請求,同源策略將不再限制 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 中列出的網(wǎng)站跨域訪問這些資源會發(fā)生什么呢?

如上圖所示,從 https://www.anotherwebsite.com 跨域訪問 https://api.mywebsite.com 資源,瀏覽器拋出一個 CORS Error,經(jīng)過上面的講解,我們可以讀懂這個報錯信息了:
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 機制就阻止了這個響應,我們無法在我們的代碼中獲取響應數(shù)據(jù)。
“CORS 還允許我們添加通配符
*作為允許的外域,這意味著該資源可以被任意外域訪問,所以要注意這種特殊情況
Access-Control-Allow-Origin 是 CORS 機制提供的眾多頭字段之一。服務器開發(fā)人員還可以通過其它頭字段擴展服務器的 CORS 策略,以允許/禁止某些請求。
另一個常見的響應頭字段是 Access-Control-Allow-Methods。其指明了跨域請求所允許使用的 HTTP 方法。

在上圖的案例中,只有GET,POST 或 PUT 方法被允許跨域訪問資源。其他 HTTP 方法,例如 PATCH 和 DELETE 都會被阻止。
“如果您想知道其它的 CORS 響應頭字段是什么以及它們的用途,可以查看此列表[4]。
說到PUT,PATCH 和 DELETE 這幾個 HTTP 方法,CORS 處理這些方法時還有些不同。這些非簡單請求會觸發(fā) CORS 的預檢請求。
4.預檢請求
CORS 有兩種類型的請求:一種是簡單請求(simple request),一種是預檢請求(preflight request)。一個跨域請求到底是簡單的的還是預檢的,取決于一些 request header。
當請求是 GET 或 POST 方法并且沒有任何自定義 Header 字段時,一般來說就是個簡單請求。除此之外的任何請求,諸如 PUT,PATCH 或 DELETE 方法,將會產(chǎn)生預檢。
“如果你想知道一個請求必須滿足哪些要求才能成為簡單請求,可以查看 MDN 簡單請求相關的文檔[5]。
說了這么多,「預檢請求」到底是什么意思?下面我們就來探討一下。
1???在發(fā)送實際請求之前,客戶端會先使用 `OPTIONS`[6] 方法發(fā)起一個預檢請求,預檢請求的 Access-Control-Request-* 中包含有關我們將要處理的實際請求的信息:
首部字段 `Access-Control-Request-Method`[7] 告知服務器,實際請求要用到的方法是什么 首部字段 `Access-Control-Request-Headers`[8] 告知服務器,實際請求將附帶的自定義請求首部字段是什么
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?? 瀏覽器收到預檢響應,并檢查是否應允許發(fā)送實際請求。

“??:上圖預檢響應漏了
Access-Control-Allow-Headers: Content-Type
4?? 如果預檢響應檢測通過,瀏覽器會將實際請求發(fā)送到服務器,然后服務器返回我們需要的資源。

如果預檢響應沒有檢驗通過,CORS 會阻止跨域訪問,實際的請求永遠不會被發(fā)送。預檢請求是一種很好的方式,可以防止我們訪問或修改那些沒有啟用 CORS 策略的服務器上的資源。
“? 為了減少網(wǎng)絡往返次數(shù),我們可以通過在 CORS 請求中添加 ?
Access-Control-Max-Age頭字段來緩存預檢響應。瀏覽器可以使用緩存來代替發(fā)送新的預檢請求。
5.認證
XHR 或 Fetch 與 CORS 的一個有趣的特性是,我們可以基于 Cookies[9] 和 HTTP 認證信息發(fā)送身份憑證。一般而言,對于跨域 XHR 或 Fetch 請求,瀏覽器不會發(fā)送身份憑證信息。
盡管 CORS 默認情況下不發(fā)送身份憑證,但我們可以通過添加 Access-Control-Allow-Credentials CORS 響應頭來更改它。
如果要在跨域請求中包含 cookie 和其他授權信息,我們需要做以下操作:
XHR 請求中將 withCredentials字段設置為trueFetch 請求中將 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.總結(jié)
CORS Error 一定程度上會讓前端開發(fā)很頭疼,但是遵循它的相關規(guī)定后,它可以讓我們在瀏覽器中進行安全的跨域請求。
同源策略和 CORS 的知識點有很多,本文只講了一些關鍵知識點,如果你想全面學習 CORS 的相關知識,我推薦你查閱MDN 文檔[10]和 W3C 規(guī)范[11],這些一手知識是最準確的。
7.最后
這篇文章就到此結(jié)束了,如果覺得不錯的話一定要點贊鼓勵一下哦,祝大家學習進步,工作順利!
如果想要學習更多非筆記式的 HTTP 知識,可以看看我之前寫的舊文:
X-Forwarded-For 拿到的就是真實 IP 嗎?[12] HTTP 請求中,空格應該被編碼為 %20 還是 + ?[13] HTTP 的這幾個坑你都踩過嗎?
??看完三件事
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點贊,讓更多的人也能看到介紹內(nèi)容(收藏不點贊,都是耍流氓-_-) 關注公眾號“前端勸退師”,不定期分享原創(chuàng)知識。 也看看其他文章
勸退師個人微信:huab119
也可以來我的GitHub博客里拿所有文章的源文件:
前端勸退指南:https://github.com/roger-hiro/BlogFN一起玩耍呀
參考資料
Lydia Hallie: https://dev.to/lydiahallie
[2]??? CS Visualized: CORS: https://dev.to/lydiahallie/cs-visualized-cors-5b8h?utm_campaign=React%2BNative%2BNow&utm_medium=web&utm_source=React_Native_Now_69#cs-cors
[3]好幾個 CORS 響應頭字段: https://fetch.spec.whatwg.org/#http-responses
[4]查看此列表: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#The_HTTP_response_headers
[5]文檔: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Simple_requests
[6]OPTIONS: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/OPTIONS
Access-Control-Request-Method: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Request-Method
Access-Control-Request-Headers: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Request-Headers
Cookies: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
[10]MDN 文檔: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
[11]W3C 規(guī)范: https://www.w3.org/wiki/CORS_Enabled
[12]X-Forwarded-For 拿到的就是真實 IP 嗎?: https://juejin.im/post/6844904174132396045
[13]HTTP 請求中,空格應該被編碼為 %20 還是 + ?: https://juejin.im/post/6844904178267979783
