摘要:而如果跨域請求是從腳本里面發(fā)出去的,由于腳本具有高度靈活性,瀏覽器出于安全考慮,會根據(jù)同源策略來限制它的功能,使得正常情況下,腳本只能請求同源的資源。反之,則稱為跨域請求,需要遵守機制。非簡單跨域請求在發(fā)出請
原文發(fā)布于我的博客:https://blog.serenader.me/htt...
自從我接觸前端以來,接手的項目里面很大部分都是前后端分離的,后端只提供接口,前端根據(jù)后端接口渲染出實際頁面。個人覺得這是一個挺好的模式,前后端各自負(fù)責(zé)各自的模塊,分工明確,而且也給前端更大的發(fā)揮空間。
與以前套模板的模式不同,前后端分離以后,前端跟后端的溝通絕大部分都是通過前端主動向后端發(fā)起請求來完成的。而前端的請求又絕大部分是由 Ajax 構(gòu)成的,Ajax 是一種非常方便的獲取數(shù)據(jù)的方式。但是,一旦 Ajax 碰上跨域,那么問題就會麻煩很多。這篇文章主要梳理了我在項目開發(fā)里面碰到的一些關(guān)于跨域請求的問題,當(dāng)然也會有一些關(guān)于跨域請求的一些背景知識。PS:文末有個小彩蛋哦?
嚴(yán)格來說,跨域請求并不僅僅只是 Ajax 的跨域請求,而是對于一個頁面來說,只要它請求了其他域名的資源了,那么這個過程就屬于跨域請求了。比如,一個帶有其他域名的 src 的 標(biāo)簽,以及頁面中引入的其他第三方的 CSS 樣式等。
對于 img 以及 CSS 而言,跨域請求本身并沒有更多的安全問題,因為這些請求都屬于只讀請求,并不會對源資源造成副作用。而如果跨域請求是從腳本里面發(fā)出去的,由于腳本具有高度靈活性,瀏覽器出于安全考慮,會根據(jù)同源策略來限制它的功能,使得正常情況下,腳本只能請求同源的資源。如果頁面確實需要通過腳本請求其他網(wǎng)站的資源,那么就應(yīng)當(dāng)在跨域資源共享(CORS)的機制下工作。
等等同學(xué),什么叫做同源策略?
同源策略(Same-origin policy)對于兩個頁面(資源)而言,只要他們滿足以下三個條件則稱他們符合同源策略:
協(xié)議相同
端口相同
域名相同
另外,about:blank 和 javascript: 繼承加載這些資源的頁面的 origin。data: 的資源不同,自身會擁有一個空的安全的上下文。
另外,子域可以通過JS 設(shè)置 document.domain 來通過同源策略。如:
在子域 http://a.example.com/test.html 的頁面中,通過 JS 設(shè)置 document.domain="example.com" ,則當(dāng)前頁面與 http://example.com/page.html 符合同源策略。
簡單的說,對于頁面 http://www.example.com/page1.html 來說,以下頁面與它都不符合同源策略,腳本無法直接請求這些資源:
https://www.example.com/page1.html : 協(xié)議不同
http://www.example.com:81/page1.html : 端口不同
http://another.example.com/page1.html : 域名不同
那么,什么又是 CORS 呢?
CORS(Cross-Origin Resource Sharing)CORS 本質(zhì)上是規(guī)定了一系列的 HTTP 頭來作為判斷腳本是否能夠?qū)崿F(xiàn)跨域請求。在了解這些請求頭之前,先來看看跨域請求有哪些類型。
通過腳本來發(fā)出請求有兩種方式,一種是通過創(chuàng)建 XMLHttpRequest 的方式來發(fā)出請求,另外一種是通過 fetch API 來實現(xiàn)請求。
一般來說,跨域請求可以大致分為兩種,其中一種稱之為簡單的請求,其符合以下條件:
請求的方法是 GET、 POST、 HEAD 其中之一。
除了瀏覽器自動帶上的請求頭(如 Connection User-Agent 等)之外,只允許下面幾種請求:頭
Accept
Accept-Language
Content-Language
Content-Type
Content-Type 請求頭的值只能是 application/x-www-form-urlencoded、 multipart/form-data、 text/plain 其中之一。
反之,如果有違背上面三條規(guī)則中的任意一條,那么即不是簡單的跨域請求。非簡單的跨域請求相對于簡單的跨域請求來說區(qū)別在于,請求在發(fā)出去之前,瀏覽器會先發(fā)送一個 preflighted 請求,用來向服務(wù)器端確認(rèn)接下來要進(jìn)行的請求是否是被允許的。
Preflight 請求在實際項目開發(fā)中,在使用 XHR 或者 fetch API 請求接口的時候很多情況下都會帶上一些額外的特殊請求頭,或者使用特殊的 HTTP 方法,如 PUT、DELETE 等(常見于 Restful 接口)。由于多了額外的請求頭或者使用了特殊的 HTTP 方法,瀏覽器就將這些請求視為非簡單的跨域請求,將會在實際請求發(fā)出去之前先自動發(fā)出一個 preflight 請求,也就是一個 OPTIONS 請求。
OPTIONS 請求會將當(dāng)前的跨域請求所使用的特殊 HTTP 請求頭和 HTTP 請求方法發(fā)送給服務(wù)器端,如 Access-Control-Request-Method 和 Access-Control-Request-Headers 。服務(wù)器端接收到 OPTIONS 請求后返回相應(yīng)的響應(yīng)頭。瀏覽器根據(jù)返回的響應(yīng)頭再來判斷該跨域請求是否被允許的。當(dāng)瀏覽器判定 OPTIONS 請求通過了,真正的請求才會發(fā)出。如以下則是一個帶有 OPTIONS 請求以及真正的 GET 請求的響應(yīng)頭和請求頭:
OPTIONS /api4 HTTP/1.1 Host: us1.serenader.me:3333 Connection: keep-alive Pragma: no-cache Cache-Control: no-cache Access-Control-Request-Method: PUT Origin: http://us1.serenader.me:3334 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36 Accept: */* Referer: http://us1.serenader.me:3334/ Accept-Encoding: gzip, deflate, sdch Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4,fr;q=0.2
HTTP/1.1 200 OK X-Powered-By: Express Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET,POST,PUT,DELETE Content-Type: text/html; charset=utf-8 Content-Length: 2 ETag: W/"2-REvLOj/Pg4kpbElGfyfh1g" Date: Thu, 19 Jan 2017 15:21:15 GMT Connection: keep-alive
PUT /api4 HTTP/1.1 Host: us1.serenader.me:3333 Connection: keep-alive Content-Length: 0 Pragma: no-cache Cache-Control: no-cache Origin: http://us1.serenader.me:3334 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36 Accept: */* Referer: http://us1.serenader.me:3334/ Accept-Encoding: gzip, deflate, sdch Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4,fr;q=0.2
HTTP/1.1 200 OK X-Powered-By: Express Access-Control-Allow-Origin: * Content-Type: text/html; charset=utf-8 Content-Length: 2 ETag: W/"2-REvLOj/Pg4kpbElGfyfh1g" Date: Thu, 19 Jan 2017 15:21:15 GMT Connection: keep-alive
了解了簡單跨域請求以及會發(fā)出 preflight 請求的非簡單跨域請求之后,我們再來看看究竟是哪些 HTTP 頭在決定這些跨域請求的「宿命」。
為了幫助讀者更好地理解這些 HTTP 頭的作用,我編寫了一個簡單的 demo ,開源在了 GitHub 上,感興趣的可以到 這個鏈接查看代碼,或者訪問這個在線 demo 預(yù)覽效果:http://us1.serenader.me:3334/。記得加載完頁面后打開 Chrome 的控制臺來查看詳細(xì)的請求信息。
Access-Control-Allow-OriginAccess-Control-Allow-Origin 是一個響應(yīng)頭,它指定了當(dāng)前資源允許被哪些域名的腳本所請求到。
跨域請求(無論簡單請求還是非簡單請求)在發(fā)出時都會帶上 Origin 請求頭,用來表明當(dāng)前發(fā)出請求的是哪一個域名。此時服務(wù)器端的響應(yīng)頭里面必須包含一個 Access-Control-Allow-Origin 并且該值匹配 Origin 請求頭,這時候該跨域請求才有可能成功。否則一律失敗。
Access-Control-Allow-Origin 是第一道門檻。其值的匹配規(guī)則是:
如果其值是通配符 * 的話,則允許所有的域名進(jìn)行跨域請求
如果其值是指定的某個固定域名,那么只允許該域名進(jìn)行跨域請求,其他域名將會失敗
如果其值是帶有通配符的域名,如 *.example.com ,那么則允許該域名以及該域名的子域名進(jìn)行跨域。
具體可以觀看 demo,demo-0 展示了當(dāng)腳本請求沒有配置跨域頭的接口時,請求被瀏覽器攔截了的情況:
demo-1 則展示了接口有配置 Access-Control-Allow-Origin 響應(yīng)頭,但是并非腳本請求的域名,此時瀏覽器會報這種錯:
只有配置了正確的 Access-Control-Allow-Origin 響應(yīng)頭請求才能夠正常接收到響應(yīng),如demo-2,此時的請求頭和響應(yīng)頭為:
GET /api2 HTTP/1.1 Host: us1.serenader.me:3333 Connection: keep-alive Pragma: no-cache Cache-Control: no-cache Origin: http://us1.serenader.me:3334 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36 Accept: */* Referer: http://us1.serenader.me:3334/ Accept-Encoding: gzip, deflate, sdch Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4,fr;q=0.2
HTTP/1.1 200 OK X-Powered-By: Express Access-Control-Allow-Origin: * Content-Type: text/html; charset=utf-8 Content-Length: 2 ETag: W/"2-REvLOj/Pg4kpbElGfyfh1g" Date: Thu, 19 Jan 2017 15:03:33 GMT Connection: keep-alive
對于簡單的跨域請求來說,通常只需要通過 Access-Control-Allow-Origin 這個響應(yīng)頭則可以請求成功(帶 cookie 等情況先不考慮,會在下面討論)。而當(dāng)請求不是簡單的跨域請求,情況就比較復(fù)雜。
Access-Control-Allow-HeadersAccess-Control-Allow-Headers 是用來告訴瀏覽器當(dāng)前接口所允許帶上的特殊請求頭是哪些。這個 HTTP 頭一般會出現(xiàn)在 OPTIONS 請求的響應(yīng)頭中。
當(dāng)請求設(shè)置了一個特殊的請求頭而且所請求的接口并沒有配置 Access-Control-Allow-Headers 響應(yīng)頭時,會報如下錯誤,如 demo-3 所示:
上面的截圖展示了請求附帶了一個 X-Custom-Header 的請求頭,但是請求在 preflight 階段就失敗了,如果要讓請求成功完成的話,則必須在 OPTIONS 請求的響應(yīng)里面配上 Access-Control-Allow-Headers: X-Custom-Header。
Access-Control-Allow-Methods與上一個 HTTP 頭相似,Access-Control-Allow-Methods 告訴瀏覽器當(dāng)前接口允許使用哪些 HTTP 方法去請求它。這個 HTTP 頭通常也是在 OPTIONS 請求的響應(yīng)頭中才有意義。當(dāng)沒有通過這個響應(yīng)頭時,會報這樣的錯誤:
同樣的,上面的截圖在 preflight 階段就失敗了。如果要讓請求成功執(zhí)行的話,那么需要配置響應(yīng)頭為:Access-Control-Allow-Methods: GET,POST,PUT。
Access-Control-Max-Age由于 OPTIONS 請求的存在,對于一個非簡單請求來說,實際發(fā)出去的請求會有兩個。這多多少少會浪費帶寬,畢竟這個校驗應(yīng)該只會在第一次發(fā)生而已,一旦通過校驗,在接下來的一段時間里,再次請求該接口的話,那么實際上 OPTIONS 請求則沒有必要再發(fā)出。
好在,有個叫做 Access-Control-Max-Age 的響應(yīng)頭可以實現(xiàn)這樣的功能。這個響應(yīng)頭指定了請求一旦通過了 preflight 請求之后,會在多長時間內(nèi)無須再次觸發(fā) preflight 請求。從而達(dá)到減少實際請求,減少帶寬浪費的問題。
Access-Control-Allow-Credentials默認(rèn)情況下, 任何跨域請求都不會帶上任何身份憑證的,這些身份憑證包括:
cookie
與身份認(rèn)證相關(guān)的請求
TLS 客戶端證書
然而,在大多數(shù)情況下,我們需要請求帶上 cookie ,那么則需要開啟跨域請求的 withCredentials 選項。
想要手動開啟傳輸 cookie 的話,有以下方法;
XHR:為 XHR對象設(shè)置 xhr.withCredentials = true 。
fetch: 傳入的參數(shù)選項里面開啟 credentials fetch(url, { credentials: "include" })
開啟了 withCredentials 之后,請求在發(fā)出去的時候就會默認(rèn)加上 Cookie。
然而,除了需要在前端中手動開啟 withCredentials 之外,服務(wù)器端也需要有相應(yīng)響應(yīng)頭支持,請求才會成功。
Access-Control-Allow-Credentials 這個響應(yīng)頭則是表明了當(dāng)前請求的資源是否允許附帶身份憑證。當(dāng)其值為 true 時請求才成功,否則會失敗,失敗內(nèi)容如下:
可以參考 demo-7觀看請求頭以及響應(yīng)頭。
另外,一旦開啟了 withCredentials 選項,服務(wù)器端的 Access-Control-Allow-Origin 響應(yīng)頭就不能是通配符,只能是固定的一個域名,否則會請求失敗。具體錯誤內(nèi)容為:
demo-8 和 demo-9 分別演示了當(dāng)請求帶上 cookie 時,響應(yīng)頭配置為通配符的情況以及響應(yīng)頭有正確配置為具體域名的情況。
總結(jié)總的來說,當(dāng)在腳本里面發(fā)出請求時,會有以下情況:
所請求資源的協(xié)議、端口或者域名如果與當(dāng)前發(fā)出請求的頁面地址一致,那么則符合同源策略,請求可以被正常發(fā)出。反之,則稱為跨域請求,需要遵守 CORS 機制。
所有跨域請求里面,服務(wù)器端必須返回 Access-Control-Allow-Origin 響應(yīng)頭,并且其值與請求中的 Origin 請求頭的值相匹配。此時請求才可以被允許,否則請求將會被瀏覽器攔截掉。
跨域請求分為兩種,一種是簡單跨域請求,另外一種是非簡單跨域請求。非簡單跨域請求在發(fā)出請求之前,瀏覽器會先發(fā)出一個 preflight 請求,即一個 OPTIONS 請求,用來驗證服務(wù)器端是否允許該請求的訪問。當(dāng) OPTIONS 請求成功時,才會繼續(xù)發(fā)送真正的請求。否則請求將會在 OPTIONS 階段便失敗了,后續(xù)真正的請求也不會發(fā)出去。
當(dāng)請求帶上了特殊的請求頭時,服務(wù)器端返回的 OPTIONS 請求的響應(yīng)必須包含 Access-Control-Allow-Headers 響應(yīng)頭,并且該值包含請求所帶上的特殊請求頭的名稱。這時候請求才會成功,否則會被瀏覽器攔截。
當(dāng)請求使用了特殊的 HTTP 方法,服務(wù)器端返回的 OPTIONS 請求的響應(yīng)必須包含 Access-Control-Allow-Methods 響應(yīng)頭,并且該值包含當(dāng)前使用的 HTTP 方法。如果沒有該響應(yīng)頭,或者當(dāng)前使用的方法并不在其值里面,則請求會被瀏覽器攔截。
因為非簡單請求每次完整請求一次資源實際上都會發(fā)出去兩個請求,為了減少 OPTIONS 請求發(fā)出的次數(shù),以便減少帶寬浪費,服務(wù)器端可以配置 Access-Control-Max-Age 來指定瀏覽器可以在多長時間內(nèi)對 OPTIONS 請求做緩存,使得一次請求成功后,下次請求相同的接口時不用再發(fā)出 OPTIONS 請求。
當(dāng)跨域請求需要帶上 cookie 等身份憑證時,需要手動開啟 withCredentials 選項,并且服務(wù)器端需要配置 Access-Control-Allow-Credentials 的響應(yīng)頭,否則請求將不會帶上任何身份憑證,或者當(dāng)沒有 Access-Control-Allow-Credentials 時請求會被瀏覽器攔截。
當(dāng)請求有帶上身份憑證時,服務(wù)器端除了需要配置 Access-Control-Allow-Credentials 響應(yīng)頭之外,Access-Control-Allow-Origin 響應(yīng)頭的值不能是通配符,必須是具體的某一個域名。否則會被瀏覽器攔截。
在以上 8 點當(dāng)中,值得注意的是第 3 點和第 8 點。
OPTIONS 請求是一個比較容易被人忽略的一個關(guān)鍵點,有一些后端人員在編寫接口的時候,往往只知道在接口的響應(yīng)頭里面寫入 Access-Control-Allow-Origin ,而沒有意識到 OPTIONS 請求的存在。特別是 OPTIONS 請求并不是每個跨域請求都會帶上的,這就導(dǎo)致了有些人會有疑問,為什么明明我發(fā)出去的是 GET 請求,結(jié)果卻是發(fā)出去了一個 OPTIONS 請求。而即使有對 OPTIONS 請求做跨域允許的話,那么也很容易因為缺少相應(yīng)的 Access-Control-Allow-Headers 或 Access-Control-Allow-Methods 響應(yīng)頭導(dǎo)致請求仍然失敗。
第 8 點也是一個非常重要的關(guān)鍵點。如果你有接口需要對多個不同域名的網(wǎng)站提供服務(wù)的話,那么你的接口就不能使用 cookie 等身份憑證了,畢竟 Access-Control-Allow-Origin 不能設(shè)置為通配符,限制了接口使用的對象。
彩蛋時間前面提到了只有非簡單請求才會觸發(fā) OPTIONS 請求,而滿足簡單請求也就只有那三個條件。但是事實并不是想象中的那么完美。
假如你使用了 XMLHttpRequest 來實現(xiàn)文件上傳的話,如果在 xhr.upload 這個對象里面添加任何事件監(jiān)聽,就會觸發(fā) OPTIONS 請求。即使此時該請求本身是滿足簡單請求的三個條件的。而一旦把事件監(jiān)聽去掉就沒有。具體可以參考 demo-10、 demo-11、 demo-12
這個「bug」是我當(dāng)初在編寫 uploader 這個庫時無意間發(fā)現(xiàn)的,我當(dāng)時還以為是瀏覽器的 bug ,但是后來在 Stackoverflow 進(jìn)行一番搜索后才發(fā)現(xiàn),原來這是瀏覽器隱藏的一個 「feature」。。
Turns out this is not a bug. The spec for XMLHttpRequest does mention that upload progress event handlers should cause the "force preflight" flag to be set. I was a bit confused when this was not specifically mentioned in the CORS spec, even though that spec does reference the existence of a "force preflight" flag.
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/81696.html
摘要:設(shè)置的值,為其當(dāng)前域或其當(dāng)前域的父域。場景文檔中的一個腳本執(zhí)行以下語句即可通過同源檢測跨源網(wǎng)絡(luò)訪問同源策略控制了不同源之間的交互。服務(wù)器確認(rèn)允許之后,才發(fā)起實際的請求。 文章大綱 同源策略 同源是什么? 如何跨源,以及場景應(yīng)用 源的更改 跨源網(wǎng)絡(luò)訪問 跨源腳本API訪問 跨源數(shù)據(jù)存儲訪問 了解CORS CORS是什么? CORS功能概述 CORS關(guān)于Cookie ...
摘要:跨域原因同源策略在客戶端編程語言中,如和,同源策略是一個很重要的安全理念,它在保證數(shù)據(jù)的安全性方面有著重要的意義。同源策略規(guī)定跨域之間的腳本是隔離的,一個域的腳本不能訪問和操作另外一個域的絕大部分屬性和方法。由兩部分組成回調(diào)函數(shù)和數(shù)據(jù)。 1.JavaScript跨域原因--同源策略 在客戶端編程語言中,如javascript和 ActionScript,同源策略是一個很重要的安全理...
摘要:實現(xiàn)跨域的原理通過方式請求載入并執(zhí)行一個文件,相當(dāng)于通過的形式的導(dǎo)入一個外部的方法語法該函數(shù)是簡寫的函數(shù),等價于在中,您可以通過使用形式的回調(diào)函數(shù)來加載其他網(wǎng)域的數(shù)據(jù),如。將自動替換為正確的函數(shù)名,以執(zhí)行回調(diào)函數(shù)。 更多詳情見http://blog.zhangbing.club/Ja... 最近在項目開發(fā)的過程中遇到一些Javascript 跨域請求的問題,今天抽空對其進(jìn)行總結(jié)一下,以...
摘要:同源策略所謂同源是指協(xié)議,域名,端口均相同。同源策略是瀏覽器的一個安全功能,不同源的客戶端腳本在沒有明確授權(quán)的情況下,不能讀寫對方資源。需注意的是由于同源策略的限制,所讀取的為跨域請求接口所在域的,而非當(dāng)前頁。 一、什么是跨域 1.URL解析 URL (Uniform Resource Locator )統(tǒng)一資源定位符(URL)是用于完整地描述Internet上網(wǎng)頁和其他資源的地址的...
閱讀 1383·2019-08-30 15:44
閱讀 2149·2019-08-30 13:49
閱讀 1752·2019-08-26 13:54
閱讀 3570·2019-08-26 10:20
閱讀 3432·2019-08-23 17:18
閱讀 3363·2019-08-23 17:05
閱讀 2202·2019-08-23 15:38
閱讀 1088·2019-08-23 14:35