跨域問題的場景和解決方案多種多樣督弓,只要是做前端開發(fā)营曼,總會(huì)遇到。而且面試時(shí)也是必問的問題愚隧。所以自己學(xué)習(xí)總結(jié)記錄一下蒂阱。
因?yàn)闉g覽器的同源策略,前端開發(fā)會(huì)遇到各種跨域問題狂塘。本篇文章總結(jié)了遇到跨域問題的不同的場景以及對(duì)應(yīng)的解決方案录煤。
前言
在總結(jié)各種跨域問題之前,我們先來了解一下瀏覽器的同源策略荞胡。
協(xié)議妈踊、域名、端口都相同才叫同源泪漂。具體的這里就不贅述了廊营。
同源政策的目的歪泳,是為了保證用戶信息的安全,防止惡意的網(wǎng)站竊取數(shù)據(jù)露筒。
設(shè)想這樣一種情況:A網(wǎng)站是一家銀行呐伞,用戶登錄以后,又去瀏覽其他網(wǎng)站慎式。如果其他網(wǎng)站可以讀取A網(wǎng)站的 Cookie伶氢,會(huì)發(fā)生什么?
很顯然瞬捕,如果 Cookie 包含隱私(比如存款總額),這些信息就會(huì)泄漏舵抹。更可怕的是肪虎,Cookie 往往用來保存用戶的登錄狀態(tài),如果用戶沒有退出登錄惧蛹,其他網(wǎng)站就可以冒充用戶扇救,為所欲為。因?yàn)闉g覽器同時(shí)還規(guī)定香嗓,提交表單不受同源政策的限制迅腔。
由此可見,"同源政策"是必需的靠娱,否則 Cookie 可以共享沧烈,互聯(lián)網(wǎng)就毫無安全可言了。
受到同源限制:
1)無法讀取不同源的 Cookie像云、LocalStorage 和 IndexDB 锌雀。
2)無法獲得不同源的DOM 。
3)不能向不同源的服務(wù)器發(fā)送ajax請(qǐng)求迅诬。
不受同源限制:
在瀏覽器中腋逆,<script>
、<img>
侈贷、<iframe>
惩歉、<link>
等標(biāo)簽都可以跨域加載資源,而不受同源策略的限制俏蛮。
瀏覽器對(duì)跨域訪問的判定:
CORS機(jī)制把跨域請(qǐng)求分為兩類:簡單請(qǐng)求和非簡單請(qǐng)求撑蚌。
- 請(qǐng)求方法是以下三種方法之一:HEAD、GET搏屑、POST
2)HTTP的頭信息不超出以下幾種字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三個(gè)值application/x-www-form-urlencoded锨并、multipart/form-data、text/plain
凡是不同時(shí)滿足上面兩個(gè)條件睬棚,就屬于非簡單請(qǐng)求第煮。瀏覽器對(duì)這兩種請(qǐng)求的處理解幼,是不一樣的。
簡單請(qǐng)求:瀏覽器會(huì)帶上Origin的請(qǐng)求頭發(fā)送到服務(wù)器包警,服務(wù)器根據(jù)Origin判斷是否許可撵摆。如果許可就會(huì)帶上CORS相關(guān)想要頭,如果不在許可范圍內(nèi)就不會(huì)帶上CORS相關(guān)的響應(yīng)頭害晦。瀏覽器再根據(jù)響應(yīng)頭中是否有相關(guān)的CORS響應(yīng)頭特铝,來判斷攔截響應(yīng)body和拋出錯(cuò)誤。
非簡單請(qǐng)求:非簡單請(qǐng)求會(huì)在發(fā)真正的請(qǐng)求之前發(fā)送一個(gè)OPTIONS的帶著Origin壹瘟、Access-Control-Request-Method鲫剿、Access-Control-Request-Headers等CORS相關(guān)的請(qǐng)求頭的預(yù)檢請(qǐng)求到服務(wù)器,服務(wù)器確認(rèn)可以這樣請(qǐng)求稻轨,就會(huì)返回帶著Access-Control-Allow-Origin灵莲、Access-Control-Allow-Methods、Access-Control-Allow-Headers等CORS相關(guān)的響應(yīng)頭的響應(yīng)殴俱,瀏覽器檢查到相關(guān)的CORS響應(yīng)頭政冻,說明通過預(yù)檢可以繼續(xù)發(fā)送真正的請(qǐng)求;服務(wù)器確認(rèn)不可以线欲,則不會(huì)返回這些相關(guān)響應(yīng)頭明场,瀏覽器沒檢查到CORS的響應(yīng)頭就會(huì)拋出錯(cuò)誤。
一李丰、代理跨域
場景1:你的項(xiàng)目myweb苦锨,myweb的前端有一個(gè)接口是去訪問一個(gè)非myweb的服務(wù)器。非myweb服務(wù)器是第三方服務(wù)器趴泌,你不能去對(duì)第三方服務(wù)器做改動(dòng)逆屡。
場景2:你的項(xiàng)目是個(gè)微服務(wù)架構(gòu)的。那你的前端頁面可能就需要去很多個(gè)服務(wù)器上訪問數(shù)據(jù)踱讨。
原理解析:
跨域請(qǐng)求報(bào)錯(cuò)歸根結(jié)底是瀏覽器禁止使用XHR對(duì)象向不同源的服務(wù)器地址發(fā)起HTTP請(qǐng)求魏蔗。如果是服務(wù)器跨域向多個(gè)不同的服務(wù)器發(fā)送請(qǐng)求就不會(huì)有跨域問題存在。因此痹筛,我們可以讓瀏覽器只向一個(gè)服務(wù)器方式請(qǐng)求莺治,讓這個(gè)服務(wù)器代替瀏覽器去不同的服務(wù)器上請(qǐng)求資源再返回給瀏覽器。這個(gè)服務(wù)器就是代理服務(wù)器了帚稠。
下面推薦一個(gè)常用代理服務(wù)器nginx谣旁。
**什么是nginx? **
Nginx (engine x) 是一款輕量級(jí)的Web 服務(wù)器 、反向代理服務(wù)器及電子郵件(IMAP/POP3)代理服務(wù)器滋早。
把ui所在的服務(wù)器和跨域服務(wù)器都用nginx代理轉(zhuǎn)發(fā)榄审,瀏覽器訪問nginx,nginx到ui服務(wù)獲取ui杆麸,再把ui下載到瀏覽器搁进,瀏覽器發(fā)起ui中的URL浪感,該URL為Nginx封裝后的跨域服務(wù)器的URL或ui服務(wù)器的URL,該URL到達(dá)Nginx之后饼问,會(huì)被轉(zhuǎn)發(fā)到跨域服務(wù)器或ui服務(wù)器影兽,請(qǐng)求處理完畢后,會(huì)通過Nginx中轉(zhuǎn)返回給瀏覽器莱革。暴露出來的或者瀏覽器所發(fā)起的url都是nginx的url峻堰,nginx去跨域服務(wù)器和ui服務(wù)器獲取響應(yīng),返給瀏覽器盅视,這樣就沒有跨域問題了捐名。
二、CORS
場景:
前后端分離的開發(fā)模式下闹击,在本地進(jìn)行接口聯(lián)調(diào)時(shí):也許在你的項(xiàng)目里镶蹋,你想嘗試前后端分離的開發(fā)模式。你在本地開發(fā)時(shí)拇砰,mock了一些假數(shù)據(jù)來幫助自己本地開發(fā)梅忌。而有一天狰腌,你希望在本地和后端同學(xué)進(jìn)行聯(lián)調(diào)除破。此時(shí),后端rd的接口地址和你發(fā)生了跨域問題琼腔。這阻止了你們的聯(lián)調(diào)瑰枫,你只能繼續(xù)使用你mock的假數(shù)據(jù)。
解決方案:
CORS需要瀏覽器和服務(wù)器同時(shí)支持丹莲。如何支持光坝?請(qǐng)看瀏覽器對(duì)跨域訪問的判定小節(jié)。
整個(gè)CORS通信過程甥材,都是瀏覽器自動(dòng)完成盯另,不需要用戶參與。對(duì)于開發(fā)者來說洲赵,CORS通信與同源的AJAX通信沒有差別鸳惯,代碼完全一樣。瀏覽器一旦發(fā)現(xiàn)AJAX請(qǐng)求跨源叠萍,就會(huì)自動(dòng)添加一些附加的頭信息芝发,有時(shí)還會(huì)多出一次附加的請(qǐng)求,但用戶不會(huì)有感覺苛谷。
因此辅鲸,實(shí)現(xiàn)CORS通信的關(guān)鍵是服務(wù)器。只要服務(wù)器實(shí)現(xiàn)了CORS接口腹殿,就可以跨源通信独悴。
服務(wù)器要給接口的響應(yīng)頭設(shè)置:Access-Control-Allow-Origin:*
三例书、jsonp
場景:跨域發(fā)送get請(qǐng)求
jsonp解決跨域問題的本質(zhì):<script>
標(biāo)簽可以請(qǐng)求不同域名下的資源,即<script>
請(qǐng)求不受瀏覽器同源策略影響绵患。
首先給body動(dòng)態(tài)添加一個(gè)<script>
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = 'http://example.com/ip?callback=foo';
document.body.appendChild(script);
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};
上面的script會(huì)向http://example.com/
服務(wù)器發(fā)送請(qǐng)求雾叭,這個(gè)請(qǐng)求的url后面帶了個(gè)callback參數(shù),是用來告訴服務(wù)器回調(diào)方法的方法名的落蝙。因?yàn)榉?wù)器收到請(qǐng)求后织狐,會(huì)把相應(yīng)數(shù)據(jù)寫進(jìn)foo的參數(shù)位置,也就是說服務(wù)器會(huì)返回的腳本如下
foo({
"ip": "8.8.8.8"
});
這樣瀏覽器通過<script>
下載的資源就是上面的腳本了筏勒,<script>
下載完成就會(huì)立即執(zhí)行移迫,也就是說http://example.com/ip?callback=foo
這個(gè)請(qǐng)求返回后就會(huì)立即執(zhí)行上面的腳本代碼,而這個(gè)腳本代碼就是調(diào)用回調(diào)方法和拿到j(luò)son數(shù)據(jù)了管行。
四厨埋、通過document.domain跨域
場景1:你的http://www.damonare.cn/a.html
頁面里使用<iframe>
調(diào)用另一個(gè)http://damonare.cn/b.html
頁面。這時(shí)候你想在a頁面里獲取b頁面里的dom捐顷,然后進(jìn)行操作荡陷。然后你會(huì)發(fā)現(xiàn)你不能獲得b的dom。document.getElementById("myIFrame").contentWindow.document
或window.parent.document.body
因?yàn)閮蓚€(gè)窗口不同源而報(bào)錯(cuò)迅涮。
解決方案:這時(shí)候你只需要在a頁面里和b頁面里把document.domain
設(shè)置成相同的值就可以在兩個(gè)頁面里操作Dom了废赞。
場景2:你在http://www.damonare.cn/a.html
頁面里寫入了document.cookie = "test1=hello";
你在http://damonare.cn/b.html
頁面是拿不到這個(gè)cookie的。
解決方案:Cookie 是服務(wù)器寫入瀏覽器的一小段信息叮姑,只有同源的網(wǎng)頁才能共享唉地。但是,兩個(gè)網(wǎng)頁一級(jí)域名相同传透,只是二級(jí)域名不同耘沼,瀏覽器允許通過設(shè)置document.domain共享 Cookie。另外朱盐,服務(wù)器也可以在設(shè)置Cookie的時(shí)候群嗤,指定Cookie的所屬域名為一級(jí)域名。這樣的話兵琳,二級(jí)域名和三級(jí)域名不用做任何設(shè)置狂秘,都可以讀取這個(gè)Cookie。
注意:
document.domain
限制:雖然可讀寫闰围,但只能設(shè)置成自身或者是高一級(jí)的父域且主域必須相同赃绊。所以只能解決一級(jí)域名相同二級(jí)域名不同的跨域問題。
document.domain
只適用于 Cookie 和 iframe 窗口羡榴,LocalStorage 和 IndexDB 無法通過這種方法跨域碧查。
五、通過window.name跨域
場景1:現(xiàn)在瀏覽器的一個(gè)標(biāo)簽頁里打開http://www.damonare.cn/a.html
頁面,你通過location.href=http://baidu.com/b.html
忠售,在同一個(gè)瀏覽器標(biāo)簽頁里打開了不同域名下的頁面传惠。這時(shí)候這兩個(gè)頁面你可以使用window.name
來傳遞參數(shù)。因?yàn)?code>window.name指的是瀏覽器窗口的名字稻扬,只要瀏覽器窗口相同卦方,那么無論在哪個(gè)網(wǎng)頁里訪問值都是一樣的。
場景2:你的http://www.damonare.cn/a.html
頁面里使用<iframe>
調(diào)用另一個(gè)http://baidu.com/b.html
頁面泰佳。這時(shí)候你想在a頁面里獲取b頁面里的dom盼砍,然后進(jìn)行操作。然后你會(huì)發(fā)現(xiàn)你不能獲得b的dom逝她。同樣會(huì)因?yàn)椴煌炊鴪?bào)錯(cuò)浇坐,和上面提到的不同之處就是兩個(gè)頁面的一級(jí)域名也不相同。這時(shí)候document.domain
就解決不了了黔宛。(這種情況我沒測試出來近刘,還需要實(shí)驗(yàn))
解決方案:瀏覽器窗口有window.name
屬性。這個(gè)屬性的最大特點(diǎn)是臀晃,無論是否同源觉渴,只要在同一個(gè)窗口里,前一個(gè)網(wǎng)頁設(shè)置了這個(gè)屬性徽惋,后一個(gè)網(wǎng)頁可以讀取它案淋。。比如你在b頁面里設(shè)定window.name="hello"
寂曹,你再返回到a頁面哎迄,在a頁面里訪問window.name
回右,可以得到hello
隆圆。
這種方法的優(yōu)點(diǎn)是,window.name容量很大翔烁,可以放置非常長的字符串渺氧;缺點(diǎn)是必須監(jiān)聽子窗口window.name屬性的變化,影響網(wǎng)頁性能蹬屹。
六侣背、通過HTML5的postMessage方法跨域
場景1:在a頁面里打開了另一個(gè)不同源的頁面b,你想要讓a和b兩個(gè)頁面互相通信慨默。比如贩耐,a要訪問b的LocalStorage。
場景2:你的a頁面里的iframe
的src是不同源的b頁面厦取,你想要讓a和b兩個(gè)頁面互相通信潮太。比如,a要訪問b的LocalStorage。
解決方案:HTML5y引入了一個(gè)全新的API铡买,跨文檔通信 API(Cross-document messaging)更鲁。這個(gè)API為window對(duì)象新增了一個(gè)window.postMessage
方法,允許跨窗口通信奇钞,不論這兩個(gè)窗口是否同源澡为。a就可以把它的LocalStorage,發(fā)送給b景埃,b也可以把自己的LocalStorage發(fā)給a媒至。
window.postMessage(message, targetOrigin, [transfer]),有三個(gè)參數(shù):
message是向目標(biāo)窗口發(fā)送的數(shù)據(jù)谷徙;
targetOrigin屬性來指定哪些窗口能接收到消息事件塘慕,其值可以是字符串"*"(表示無限制)或者一個(gè)URI(或者說是發(fā)送消息的目標(biāo)域名);
transfer可選參數(shù)蒂胞,是一串和message 同時(shí)傳遞的 Transferable 對(duì)象. 這些對(duì)象的所有權(quán)將被轉(zhuǎn)移給消息的接收方图呢,而發(fā)送一方將不再保有所有權(quán)。
另外消息的接收方必須有監(jiān)聽事件骗随,否則發(fā)送消息時(shí)就會(huì)報(bào)錯(cuò)蛤织。The target origin provided ('http://localhost:8080') does not match the recipient window's origin ('http://localhost:63343').
window.addEventListener("message",onmessage);
onmessage接收到的message事件包含三個(gè)屬性:
data:從其他 window 中傳遞過來的數(shù)據(jù)。
origin:調(diào)用 postMessage 時(shí)消息發(fā)送方窗口的 origin 鸿染。請(qǐng)注意指蚜,這個(gè)origin不能保證是該窗口的當(dāng)前或未來origin,因?yàn)閜ostMessage被調(diào)用后可能被導(dǎo)航到不同的位置涨椒。
source:對(duì)發(fā)送消息的窗口對(duì)象的引用; 您可以使用此來在具有不同origin的兩個(gè)窗口之間建立雙向通信摊鸡。
例子:我在a頁面執(zhí)行
var popup = window.open('http://localhost:3000', 'title');
popup.postMessage('Hello World!', 'http://localhost:3000');
同時(shí)在http://localhost:3000的頁面里監(jiān)聽message事件:
window.onload=function () {
window.addEventListener("message",onmessage);
}
function onmessage(event) {
if(event.origin=="http://localhost:63343"){//http://localhost:63343是發(fā)送方a的域名
console.log(event.data);//'Hello World!'
}
console.log(event.data);//'Hello World!'
}
注意: 在 Gecko 6.0 (Firefox 6.0 / Thunderbird 6.0 / SeaMonkey 2.3)之前, 參數(shù) message 必須是一個(gè)字符串蚕冬。 從 Gecko 6.0 (Firefox 6.0 / Thunderbird 6.0 / SeaMonkey 2.3)開始免猾,參數(shù) message被使用結(jié)構(gòu)化克隆算法進(jìn)行序列化。這意味著您可以將各種各樣的數(shù)據(jù)對(duì)象安全地傳遞到目標(biāo)窗口囤热,而不必自己序列化它們猎提。
七、 通過location.hash跨域
location.hash就是指URL的#號(hào)后面的部分旁蔼。
場景:
父窗口和iframe的子窗口之間通訊或者是window.open打開的子窗口之間的通訊锨苏。
解決方案:
父窗口改變子窗口的url的#號(hào)后面的部分,后者把要傳遞的參數(shù)寫在#后面棺聊,子窗口監(jiān)聽window.onhashchange事件伞租,得到通知,讀取window.location.hash解析出有用的數(shù)據(jù)限佩。同樣子窗口也可以向父窗口傳遞數(shù)據(jù)葵诈。
參考資料:
瀏覽器同源策略與ajax跨域方法匯總
前端跨域整理
瀏覽器同源政策及其規(guī)避方法
跨域資源共享 CORS 詳解