【Geek議題】當年那些風騷的跨域操做

前言

如今cross-origin resource sharing(跨域資源共享,下簡稱CORS)已經十分普及,算上IE8的不標準兼容(XDomainRequest),各大瀏覽器基本都已支持,當年爲了先後端分離、iframe交互和第三方插件開發而頭疼跨域是時代已通過去,但當年爲了跨域無所不用其極的風騷操做卻依然值得學習。
本篇文章不是從實用的角度考量這些舊時代的跨域手段,而是更偏向理論的闡述,並引起對瀏覽器安全的思考,由於跨域實際上也是各種攻擊的核心。
本人我的能力有限,歡迎大牛一塊兒討論,批評指正。javascript

同源策略

1995年,同源政策由Netscape公司引入瀏覽器。目前,全部瀏覽器都實行這個安全策略。
核心是確保不一樣源提供的文件(資源)之間是相互獨立的。換句話說,只有當不一樣的文件腳本是由相同的域、端口、HTTP協議提供時,纔沒有特殊的限制。特殊限制能夠細分爲兩個方面:html

  • 對象訪問限制:主要體如今iframe,若是父子頁面的源是不一樣的,那就不能夠訪問對方的DOM方法和屬性(包括Cookie、LocalStorage和IndexDB等)。不一樣來源便拋出異常。
  • 網絡訪問限制:主要體如今AJAX請求,若是發起的請求目標源與當前頁面不一樣,瀏覽器就會限制了發起跨站請求,或攔截返回的請求。

一個表格看懂什麼是同源?前端

origin(URL) result reason
http://example.com success 協議,域名和端口號80均相同
http://example.com:8080 fail 端口不一樣
https://example.com fail 協議不一樣
http://sub.example.com fail 域名不一樣

至於爲何說這是個安全策略?
這個就要提到cookie-session機制,衆所周知HTTP是無狀態協議,而服務器如何知曉用戶的登陸狀態?傳統上是使用了cookie-session這一機制,也就是服務器爲每一個訪問者生成了一個session標識,而session標識會被服務器包含在應答頭中返回,瀏覽器解析到應答頭中的set-cookie就把這串session標識保存到本地cookie中,利用cookie每次請求同一個域都會帶上的特性,服務器器就能知曉當前的用戶登陸狀態。
因此若是讓瀏覽器向不一樣源發起請求,就會形成很大的危險。好比用戶登陸了銀行的網站A,也就是說A站已經在瀏覽器留下了cookie,這時候用戶又訪問了B站,若是能在B站頁面上發起A站的請求,就至關於B站能夠冒充用戶,在A站隨心所欲。
因而可知,"同源策略"是必需的,不然cookie能夠共享,互聯網就毫無安全可言了。java

跨域方案

同源策略提出的時代仍是傳統MVC架構(jsp,asp)盛行的年代,那時候的頁面靠服務器渲染完成了大部分填充,內容也比較簡單,開發者也不會維護獨立的API工程,因此其實跨域的需求是比較少的。
新時代先後端的分離和第三方JSSDK的興起,咱們纔開始發現這個策略雖然大大提升了瀏覽器的安全性,但有時很不方便,合理的用途也受到影響。好比:jquery

  1. 獨立的API工程部署爲了方便管理使用了獨立的域名;
  2. 前端開發者本地調試須要使用遠程的API;
  3. 第三方開發的JSSDK須要嵌入到別人的頁面中使用;
  4. 公共平臺的開放API。

因而乎,在沒有標準規範的時代,如何解決這些問題的跨域方案就被紛紛提出,可謂百家爭鳴,其中不乏使人驚歎的騷操做,這樣的極客精神依然值得咱們敬佩和學習。ajax

JSON-P

JSON-P是各種跨域方案中流行度較高的一個,如今在某些要兼容舊瀏覽器的環境下還會被使用,著名的jQuery也封裝其方法。請勿見名知義,名字中的P是padding「帶填充」的意思,這個方法在通訊過程當中使用的並非普通的json,而是自帶填充功能的JavaScript腳本
如何理解「自帶填充功能的JavaScript腳本」,看看下面的例子或許比較簡單,若是一個js文件裏這樣寫並被引入,則全局下就會有data對象,也就是說利用js腳本的引入和解析能夠用來傳遞數據,若是把js腳本換成函數運行命令豈不是能夠調用全局函數了。這就是JSON-P方法的核心思想,它填充的是全局函數的數據。json

var data = {
  a: 1,
  b: 2
}
【PS】 <script>標籤不受同源策略限制,但只能發起get請求。

原理及流程後端

  1. 先定義好回調函數,也就是引入的js腳本中要調用的函數;
  2. 新建<script>標籤,將標籤插入頁面瀏覽器便會發起get請求;
  3. 服務器根據請求返回js腳本,其中調用了回調函數。

jsonp流程圖

// 定義回調函數
function getTheAnimal(data){
    var myAnimal = data.animal;
}
// 新建標籤
var script = document.createElement("script");
script.type = "text/javascript";
// 經常使用的在url參數部分跟服務器約定號回調函數名
script.src = "http://demo.com/animal.json?callback=getTheAnimal";
document.getElementByTagName('head')[0].appendChild(script);

總結 api

優勢:跨域

  • 簡單,有現成的工具庫(jQuery)支持;
  • 支持上古級別的瀏覽器(IE8-)。

缺點:

  • 只能是GET方法;
  • 受瀏覽器URL最大長度2083字符限制;
  • 沒法調試,服務器錯誤沒法檢測到具體緣由;
  • 有CSRF的安全風險;
  • 只能是異步,沒法同步阻塞;
  • 須要特殊接口支持,不能基於REST的API規範。

子域名代理

這個方法其實是利用瀏覽器容許iframe內的頁面只要是跟父頁面是同個一級域名下,就能被父頁面修改和調用的特色。也許你會疑問,上面講同源策略的表格中很明確二級域名不一樣也是算不一樣源,這豈不矛盾了?
這其實不矛盾,若是正常操做確實會被同源策略限制,但瀏覽器的document.domain容許網站將主機部分更改成原始值的後綴。這意味着,寄放在sub.example.com的頁面能夠將它的源設置爲example.com,可是並不能將其設置爲alt.example.com或google.com。

【PS】這裏有一個細節,父子頁面均要設置 document.domain才能被互相訪問,單一一個是沒法跨域的。 document.domain的特色:只能設置一次;只能更改域名部分,不能修改端口號和協議;重置源的端口爲協議默認端口。

原理及流程

  1. 新建一個子域,好比api.demo.com(頁面在主域名demo.com下);
  2. 子域下須要一個代理文件proxy.html,設置其document.domain = 'demo.com',並能夠包含發起ajax的工具;
  3. 全部API地址都是在api.demo.com;
  4. 把須要發請求的主域頁面設置其document.domain = 'demo.com'
  5. 新建iframe標籤連接到代理頁;
  6. 當iframe內的子頁面就緒時,父頁面就可使用子頁面發起ajax請求。

子域名代理流程圖

// 最簡單的代理文件proxy.html
<!DOCTYPE html>
<html>
    <script>
        document.domain = 'demo.com';
    </script>
    <script src="jquery.min.js"></script>
</html>
// 新建iframe
var iframe = document.createElement('iframe');
// 連接到代理頁
iframe.src = 'http://api.demo.com/proxy.html';
// 代理頁就緒時觸發
iframe.onload = function(){
  // 因爲代理頁已經和父頁設置了相同的源,父的腳本能夠調用代理頁的ajax工具;
  // 因爲是在子頁面發起,其請求地址就跟子頁面同源了。
  iframe.contentWindow.jQuery.ajax({
    method: 'POST',
    url: 'http://api.demo.com/products',
    data: {
      product: id,
    },
    success: function(){
      document.body.removeChild(iframe);
      /*...*/
    }
  })
}
document.getElementsByTagName('head')[0].appendChild(iframe);

總結

優勢:

  • 能夠發送任意類型的請求;
  • 可使用基於REST的API規範。

缺點:

  • 不太適合第三方API,給第二方使用較麻煩;
  • iframe對瀏覽器性能影響較大;
  • 沒法使用非協議默認端口的API。

模擬form表單

form表單的target屬性能夠指定一個iframe,使主頁面不跳轉,而iframe內跳轉,因此這個方法的核心就是利用表單提交,並在iframe中獲取數據
要訪問iframe內外頁面互訪也是必須設置同源,這點與子域代理是類似的;而iframe內回調父頁面,又與JSON-P類似,能夠說是兩個思路的合體版。
form表單提交後返回的是頁面,因此與JSON-P不一樣的是,返回的是包含了自帶填充功能的JavaScript腳本的頁面,提及來有點繞,簡單來講就是把JSON-P返回的腳本放到一個html頁面裏自運行。
相比子域代理的方法,它不須要代理頁

【PS】form表單提交的特色就是會致使整個頁面跳轉,返回數據是在新的頁面上,這樣天然不會產生跨域的問題。

原理及流程

  1. 新建一個子域,好比api.demo.com(頁面在主域名demo.com下);
  2. 全部API地址都是在api.demo.com;
  3. 把須要發請求的主域頁面設置其document.domain = 'demo.com'
  4. 先定義好父頁面上的回調函數;
  5. 新建iframe標籤並指定名字;
  6. 新建表單form標籤,指定target爲剛纔的iframe,並添加數據;
  7. 提交表單,iframe內跳轉,其中自運行腳本調用了父頁面的回調函數。

模擬form表單流程圖

// 新建並隱藏iframe
var frame = document.createElement('iframe');
iframe.name = 'post-review';
frame.style.display = 'none';

// 新建表單
var form = document.createElement('form');
form.action = 'http://api.demo.com/products';
form.method = 'POST';
form.target = 'post-review';
// 添加數據
var score = document.createElement('input');
score.name = 'score';
score.value = '5';
// 添加數據
var message = document.createElement('input');
message.name = 'message';
message.value = 'hello world';
// 把數據加到表單
form.appendChild(score);
form.appendChild(message);
// 渲染iframe和表單
document.body.appendChild(frame);
document.body.appendChild(form);
// 提交表單發起請求
form.submit();
// 完成清理元素
document.body.removeChild(form);
document.body.removeChild(frame);
// 最簡單返回html
<!DOCTYPE html>
<html>
    <script>
        document.domain = 'demo.com';
        window.parent.jsonpCallback('{"status":"success"}');
    </script>
</html>

總結

因爲這個方法是JSON-P與子域名代理的結合版,能夠說即擁有二者的優勢,也保留了二者一些缺點。

優勢:

  • 能夠發送任意類型的請求;
  • 不須要代理頁;
  • 支持上古級別的瀏覽器(IE8-)。

缺點:

  • 不太適合第三方API,給第二方使用較麻煩;
  • iframe對瀏覽器性能影響較大;
  • 沒法使用非協議默認端口的API;
  • 須要特殊接口支持,不能基於REST的API規範。

window.name

這方法利用了window.name的特性:一旦被賦值後,當窗口被重定向到一個新的URL時不會改變它的值。這一行爲使得不一樣域的特定文檔能夠讀取該屬性值,所以能夠繞過同源策略並使跨域消息通訊成爲可能。

【PS】例子裏演示的是發起get請求,只要把請求地址直接寫到src裏就好了。若是想要發起其餘類型的請求,能夠類比採用模擬的form的方式進行改造。

原理及流程

  1. 新建iframe,使用iframe訪問一個非同源的地址(發請求);
  2. 當頁面加載完成後,iframe內腳本給window.name屬性賦值,這時父頁面仍是不能讀取到子頁面的屬性(由於不一樣源);
  3. iframe自身回調到一個同源的地址(可能只是個空白頁),這時候window.name沒有改變;
  4. 父頁面順利讀取window.name的值。

window.name流程圖

// 新建iframe
var iframe = document.createElement('iframe');
var body = document.getElementByTagName('body');
// 隱藏iframe並連接地址
iframe.style.display = 'none';
iframe.src = 'http://api.demo.com/server.html?id=1';
// 由於須要兩次跳轉,這裏有個完成標記
var done = fasle;
// 這裏會觸發至少兩次,一次因爲非同源是取不到值的。
iframe.onload = iframe.onreadystatechange = function(){
    if(! this.readyState && (iframe.readyState !== 'complete' || done)){
        return;
    }
    console.log('Listening');
    var name = iframe.contentWindow.name;
    if(name){
        console.log(iframe.contentWindow.name);
        done = true;
    }
};
body.appendChild(iframe);
// 最簡單返回html
<!DOCTYPE html>
<html>
    <script>
    function init(){
        window.name = 'hello';
        window.location = 'http://demo.com/empty.html'
    }
    </script>
    <body onload="init();"></body>
</html>

總結

優勢:

  • 能夠發送任意類型的請求;
  • 不須要設置子域名。

缺點:

  • iframe對瀏覽器性能影響較大;
  • 須要特殊接口支持,不能基於REST的API規範;
  • 每當你想要獲取一條新的消息時都不得不發起兩次網絡請求,網絡成本大;
  • 須要準備空白頁,對它的訪問是無心義的,影響流量統計。

window.hash

這個方法利用了location的特性:不一樣域的頁面,能夠寫不可讀。而只改變哈希部分(井號後面)不會致使頁面跳轉。也就是可讓父、子頁面互相寫對方的location的哈希部分,進行通信。

原理及流程

  1. 新建iframe,使用iframe訪問一個非同源的地址(發請求),參數裏帶上父頁面url;
  2. 當頁面加載完成後,iframe內腳本設置父頁面的url並在哈希部分帶上數據;
  3. 父頁面的腳本循環檢查哈希值的變化,若是檢查到有值就取值並清空哈希值;
【PS】父頁面會循環檢查哈希是否改變來讀取值,由於這種降級方案的使用環境通常是不會有hashchange事件的。演示裏是最簡單的get方法,若是想要發起其餘類型的請求,能夠類比採用模擬的form的方式進行改造,但記住不要丟失父頁面的url。

window.hash流程圖

// 獲取當前url
var url = window.location.href;
// 新建iframe
var iframe = document.createElement('iframe');
// 隱藏iframe並設置連接,把當前url帶上
iframe.style.display = 'none';
iframe.src = 'http://api.demo.com/server.html?id=1&url=' + encodeURIComponent(url);

var body = document.getElementByTagName('body')[0];
body.appendChild(iframe);
// 循環監聽處理
var listener = function(){
    // 讀取
    var hash = location.hash;
    // 還原
    if(hash && hash !== '#'){
        console.log(hash.replace('#', ''));
        window.loacation.href = url + '#';
    }
    // 繼續監聽
    setTimeout(listener, 100);
};
listener();
// 最簡單返回html
<!DOCTYPE html>
<html>
    <script>
    function init(){
        // 剪裁出父頁面的url
        var parentUrl = '';
        var url = window.location.href;
        var str = url.split('?')[1].replace('?', '');
        strs = str.split("&");
        for(var i = 0; i < strs.length; i ++) {
            if(strs.split("=")[0] === 'url'){
                parentUrl = strs.split("=")[1];
            }
        }
        // 設置到父頁面上
        window.parent.location = decodeURIComponent(parentUrl) + '#helloworld';
    }
    </script>
    <body onload="init();"></body>
</html>

總結

優勢:

  • 能夠發送任意類型的請求;
  • 不須要設置子域名。

缺點:

  • iframe對瀏覽器性能影響較大;
  • 須要特殊接口支持,不能基於REST的API規範;
  • 循環檢查哈希須要消耗性能;
  • 返回數據受瀏覽器URL最大長度2083字符限制。

現代的標準

W3C的標準化跨域方案,讓現代瀏覽器跨域已經不是什麼複雜的事。這部分網上資料已經不少,這裏就只是簡單介紹。

CORS

CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)。
它容許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。

CORS參考文檔
跨域資源共享 CORS 詳解

postMessage

H5的window.postMessage爲瀏覽器帶來了一個安全的。基於事件的消息api。
只要是window對象,基本均可以使用這個方法,也就是說window.name、window.hash這類風騷的操做都已成爲降級方案。

postMessage參考文檔

安全問題

上述的各種非標準的騷操做,都算是對同源策略的破解辦法,在方便開發者完成跨域目的的同時,各種惡意的攻擊者也天然會利用這些方案爲非做歹。 其中子域名代理的風險最低,由於須要服務器設置特定的子域名,也就是已是兩個源的協商結果,通常黑客是難以模擬的。 風險最高的要算JSON-P的方案,由於這是任何客戶端均可隨意使用的辦法,CSRF攻擊的核心也是利用了特定標籤的跨域性發起請求,因此JSON-P最好用在無用戶狀態的低安全性API上。

相關文章
相關標籤/搜索