前言
熟悉web前端開發(fā)的人都知道,瀏覽器在請求不同域的資源時,會受到瀏覽器的同源策略影響,請求資源有可能不成功,這也就是我們前端常常提到的跨域問題。這類問題往往會拖延項目推進,困擾著前端開發(fā)者。本文將從技術層面全面解析跨域問題的由來、實戰(zhàn)經驗,以及方法總結。
1.跨域問題的由來
首先我們需要了解,前端處于項目開發(fā)過程中最接近用戶的區(qū)域,代碼最容易被hack獲取解析,也最容易受到攻擊。針對這個問題,互聯(lián)網(wǎng)早期探索者Netscape提出了一個著名的安全策略——同源策略:瀏覽器限制腳本中發(fā)起的跨站請求,要求JavaScript或cookie只能訪問同源的資源。這里的同源指的是域名、協(xié)議名以及端口號相同。正是由于這個機制,才使得我們無法用簡單的手段來請求不同域名下的資源。
2.跨域資源共享CORS
CORS是W3C提出的一個標準——跨域資源共享(Cross-Origin Resource Sharing)。它允許瀏覽器向跨域服務器發(fā)出XML Http Request請求,從而克服AJAX只能同源使用的限制。
首先CORS需要瀏覽器和服務器同時支持,現(xiàn)代瀏覽器包括IE10+都支持CORS請求。
圖1 CORS瀏覽器支持進度
使用CORS跨域和普通的AJAX過程是一樣的。瀏覽器一旦發(fā)現(xiàn)AJAX請求跨域資源,就會自動添加一些請求頭幫助我們處理一些事情。所以只要服務端提供CORS支持,前端不需要做額外的事情。
CORS請求分兩種,這里簡要介紹其中一種:
1)簡單請求(simple request)
滿足以下兩個條件,就屬于簡單請求。
a.請求方式是head,get,post三者中其一;
b.http請求頭信息不超出以下字段:Accept、Accept-Language、Last-Event-ID、Content-Type:只限于application/x-www-form-urlencoded、multipart/form-data和text/plain。
瀏覽器在進行簡單請求時,伴隨著ajax請求的產生,瀏覽器會自動添加origin字段,表明請求來源。服務器會識別出源,并且決定是否返回數(shù)據(jù)給該源。
圖2 瀏覽器自動添加origin字段
如果origin并不在服務器許可范圍內,服務器會返回一個正常的http。瀏覽器接受后發(fā)現(xiàn)這個http的頭信息中不包含Access-Control-Allow-Origin字段,就知道出錯了,隨后在瀏覽器會拋出相應的error。
圖3 origin不被服務器認可從而拋出error
這里列出幾個返回http中常見的幾個CORS請求頭:
- Access-Control-Allow-Origin:該字段為必需字段,可以是指定的源名(協(xié)議+域名+端口),也可以使用通配符*代表接受所有跨域資源請求
- Access-Control-Allow-Credentials:該字段為boolean值,表示是否允許發(fā)送cookie,默認為false,即不允許發(fā)送cookie值。
- Access-Control-Expose-Headers:該字段可選。CORS請求時,XMLHttpRequest對象的getResponseHeader()方法只能拿到6個基本字段:Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma.如果想拿到其他的字段,必須在Access-Control-Expose-Headers里面指定。
圖4 CORS請求成功后,服務其返回成功的請求頭
2)非簡單請求
非簡單請求會對服務器有特殊要求,在正式通信之前,會增加一次http查詢請求,額外的占用資源,并影響到請求速度。達觀數(shù)據(jù)在數(shù)據(jù)處理以及返回數(shù)據(jù)的過程中對性能有著極高的要求,在實際項目中并沒有嘗試這種實現(xiàn)方式。筆者本人也并未對此做過深入學習,在此就不班門弄斧了。
3.使用jsonp進行跨域請求
Jsonp可以說是目前前端跨域問題最普遍的解決方案了。首先簡要介紹一下jsonp概念,jsonp跟json只有一字母之差,卻完全是兩個概念,json是一種數(shù)據(jù)存儲的基本格式,通常見于js腳本存儲數(shù)據(jù),ajax請求數(shù)據(jù)。而jsonp是一種非正式的傳輸協(xié)議,該協(xié)議的一個要點是允許用戶傳遞一個callback參數(shù)給服務端,服務端返回數(shù)據(jù)時,會將callback參數(shù)作為函數(shù)名來包裹住JSON數(shù)據(jù),這樣客戶端就可以隨意定制自己的函數(shù)來自動處理返回數(shù)據(jù)了。
Jsonp的原理是:普通資源請求都會受到跨域影響,但含有src屬性的跨域資源在調用時并不會受到影響,Jsonp就是由于這種特性被發(fā)掘并實現(xiàn)跨域的。
在使用jsonp進行跨域請求時,需要注冊一個callback回調函數(shù),這個函數(shù)接受到一個參數(shù),之后在瀏覽器中動態(tài)生成一個script標簽,,并在請求的src中加入我們的callback名稱。
圖5 在本地定義callback函數(shù)
例如:callback名為alert Message,在頁面中動態(tài)添加src為35285.cn? callback=alert Message的script標簽。這樣,一條請求就向服務端發(fā)送成功了,服務端在接收并識別出callback后,將想要返回的數(shù)據(jù)動態(tài)的包裹在callback括號內。
圖6? Jsonp請求成功后返回的腳本內容
Script加載成功后,會執(zhí)行本地alert Message方法,將最終的結果alert出來。本質上,jsonp就是將需要執(zhí)行的函數(shù)名傳遞給服務端,在服務端將對應的數(shù)據(jù)包裝到函數(shù)參數(shù)域內,并返回到本地進行調用的過程。
4.小眾跨域方式
除了CORS和jsonp之外還有一些比較小眾的跨域方式,一起整理分享給各位讀者。
1)document.domain
頁面中的iframe和其父頁面的window對象是可以互相獲取到的(盡管取到的window對象不能拿到方法和屬性)。但是我們可以通過修改document.domain這一屬性,來使獲得window對象具有方法和屬性。這里需要注意的是,iframe和其父頁面的主域名必須相同。例如,在35285.cn/index.html頁面中嵌入一個src為shilieyu.datagrand.com/index.html的iframe,同時修改兩個頁面的document.domain為datagrand.com。這樣就可以在互相獲取到對方頁面的window對象中,且存在方法和屬性了。這時,在其中一個頁面中可以使用ajax請求數(shù)據(jù),另一個頁面就可以使用window對象獲取到對應數(shù)據(jù)。
2)window.postMessage
Post Message為html5中引進的方法,該方法可以向其他window對象發(fā)送消息,無論這個window對象是否同源。
圖7 possMessage支持進度
首先介紹一下postMessage方法的兩個參數(shù),postMessage接受兩個參數(shù),第一個為要發(fā)送的數(shù)據(jù),該參數(shù)只能為字符串類型,第二個參數(shù)用來限定接受消息的window對象所在域。如果不想限定,可以使用通配符*允許所有域接受該消息。需要接收消息的window對象,需要監(jiān)聽自身的message事件,來獲取傳過來的消息。事件觸發(fā)時,可以通過接受參數(shù)的data值,來獲取對應的數(shù)據(jù)。舉例,如下圖所示,在a頁面中創(chuàng)建指向b頁面的iframe并在其onload階段調用postMessage方法,隨后在iframe完成時,頁面會alert出a頁面?zhèn)鬟f過去的值。也就意味著通過postMessage方法跨域成功了。
圖8 使用postMessage的a頁面
圖9 接受postMessage的b頁面(http://shilieyu.datagrand.net/index.html)
3)window.name
Window的name屬性有個很有趣的特點,在一個窗口(window)的生存周期內,所有頁面共享一個name屬性,每個頁面對window.name都有讀寫的權限,這就意味著,在頁面即將發(fā)生跳轉時,可以將想要傳遞的數(shù)據(jù)放入window.name中,頁面跳轉成功后,新頁面可以通過window.name獲取前頁面?zhèn)鬟f的值。
利用這種特性,可以在a頁面通過iframe的形式,先訪問存儲數(shù)據(jù)頁面,將請求值存入iframe的window.name中,再將src設置為與a頁面同源的頁面,否則是無法通過window獲取到iframe中的屬性的(詳見window.domain中內容)
5.總結
跨域作為一個前端開發(fā)中經常會遇到的門檻,常常困擾著開發(fā)者們。本文對跨域問題的產生以及如何解決跨域問題進行了總結,也是希望讀者在遇到相似的困境時,能有一個完整清晰的解決思路。
6.作者簡介
施列宇,15年畢業(yè)于西安電子科技大學,專業(yè)軟件工程。目前就職于達而觀信息科技(上海)有限公司,任職前端開發(fā)工程師,負責大數(shù)據(jù)平臺的pc與webapp研發(fā)工作。曾在上海葡萄城信息技術有限公司wijmo項目組負責wijmo控件前端的維護以及控件在Asp.Net平臺的衍生工作。對web兼容性,前端性能提升,SEO,響應式設計以及主流前端框架有著豐富的實戰(zhàn)經驗。
// // <![CDATA[
(function() {
function _addVConsole(uri) {
var url = ‘//res.wx.qq.com/mmbizwap/zh_CN/htmledition/js/vconsole/’ + uri;
document.write(”);
}
if (
(document.cookie && document.cookie.indexOf(‘vconsole_open=1’) > -1)
|| location.href.indexOf(‘vconsole=1’) > -1
) {
_addVConsole(‘2.3.1/vconsole.min.js’);
_addVConsole(‘plugin/vconsole-sources/1.0.1/vconsole-sources.min.js’);
_addVConsole(‘plugin/vconsole-resources/1.0.0/vconsole-resources.min.js’);
_addVConsole(‘plugin/vconsole-mpopt/1.0.0/vconsole-mpopt.js’);
}
})();
// ]]>// // // <![CDATA[
var occupyImg = function() {
var images = document.getElementsByTagName(‘img’);
var length = images.length;
var container = document.getElementById(‘img-content’);
var max_width = container.offsetWidth;
var container_padding = 0;
if (typeof getComputedStyle != “undefined”) {
var container_style = getComputedStyle(container)
container_padding = parseInt(container_style.paddingLeft) + parseInt(container_style.paddingRight);
} else if (container.currentStyle) {
container_padding = parseInt(container.currentStyle.paddingLeft) + parseInt(container.currentStyle.paddingRight);
}
max_width -= container_padding;
if (!max_width) {
max_width = window.innerWidth – 30;
}
for (var i = 0; i 0) {
if (images[i].style.width) images[i].setAttribute(‘_width’, images[i].style.width);
var width = width_ > max_width ? max_width : width_;
height = width * ratio_;
images[i].style.cssText += “width: ” + width + “px !important;”;
images[i].src = “data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkJDQzA1MTVGNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkJDQzA1MTYwNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QkNDMDUxNUQ2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QkNDMDUxNUU2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6p+a6fAAAAD0lEQVR42mJ89/Y1QIABAAWXAsgVS/hWAAAAAElFTkSuQmCC”;
} else {
images[i].style.cssText += “visibility: hidden !important;”;
}
images[i].style.cssText += “height: ” + height + “px !important;”;
}
}
if (document.addEventListener) {
document.addEventListener(‘DOMContentLoaded’, function() {
occupyImg();
});
} else {
document.onreadystatechange = function() {
if (document.readyState === ‘complete’) {
occupyImg();
}
}
}
// ]]>//