1. 程式人生 > >前後端跨域問題解決方式

前後端跨域問題解決方式

由於安全的原因,瀏覽器做了很多方面的工作,由此也就引入了一系列的跨域問題,需要注意的是:

跨域並非瀏覽器限制了發起跨站請求,而是跨站請求可以正常發起,但是返回結果被瀏覽器攔截了。最好的例子是 CSRF 跨站攻擊原理,請求是傳送到了後端伺服器無論是否跨域!注意:有些瀏覽器不允許從HTTPS的域跨域訪問HTTP,比如Chrome和Firefox,這些瀏覽器在請求還未發出的時候就會攔截請求,這是一個特例

1. JSONP

JSONP的全稱是 "JSON With Padding", 詞面意思上理解就是 "填充式的JSON"。它不是一個新鮮的東西,隸屬於 JSON 的一種使用方法,或者說是一種使用模式,可以解決一些常見的瀏覽器端網頁跨域問題。

正如他的名稱一樣,它是指被包含在呼叫函式中的JSON,比如這樣:

callback({"Name": "小明", "Id" : 1823, "Rank": 7})

由於 jQuery 的一些原因,使得 JSONP 常常與 Ajax 混淆。實際上,他們沒有任何關係。

由於瀏覽器的同源策略,使得在網頁端出現了這個“跨域”的問題,然而我們發現,所有的 src 屬性並沒有受到相關的限制,比如 img / script 等。

JSONP 的原理就要從 script 說起。script 可以執行其他域的js 函式,比如這樣:

a.html
...
<script>
  function callback(data) {
    console.log(data.url)
  }
</script>

<script src='b.js'></script>
...


b.js
callback({url: 'http://www.rccoder.net'})

顯然,上面的程式碼是可以執行的,並且可以在console裡面輸出http://www.rccoder.net

利用這一點,假如b.js裡面的內容不是固定的,而是根據一些東西自動生成的, 嗯,這就是JSONP的主要原理了。回撥函式+資料就是 JSON With Padding 了,回撥函式用來響應應該在頁面中呼叫的函式,資料則用來傳入要執行的回撥函式。

至於這個資料是怎麼產生的,說粗魯點無非就是字串拼接了。

簡單總結一下: Ajax 是利用 XMLHTTPRequest 來請求資料的,而它是不能請求不同域上的資料的。但是,在頁面上引用不同域的 js 檔案卻是沒有任何問題的,這樣,利用非同步的載入,請求一個 js 檔案,而這個檔案的內容是動態生成的(後臺語言字串拼接出來的),裡面包含的是 JSON With Padding(回撥函式+資料),之前寫的那個函式就因為新載入進來的這段動態生成的 js 而執行,也就是獲取到了他要獲取的資料。

重複一下,在一個頁面中,a.html這樣寫,得到 UserId 為 1823 的資訊:

a.html

...
src="http://server2.example.com/RetrieveUser?UserId=1823&callback=parseResponse">
...

請求這個地址會得到一個可以執行的 JavaScript。比如會得到:

  parseResponse({"Name": "小明", "Id" : 1823, "Rank": 7})

這樣,a.html裡面的 parseResponse() 這個函式就能執行並且得到資料了。

等等,jQuery到底做了什麼:

jQuery 讓 JSONP 的使用API和Ajax的一模一樣:

$.ajax({
  method: 'jsonp',
  url: 'http://server2.example.com/RetrieveUser?UserId=1823',
  success: function(data) {
    console.log(data)
  } 
})

之所以可以這樣是因為 jQuery 在背後傾注了心血,它會在執行的時候生成函式替換callback=dosomthing ,然後獲取到資料之後銷燬掉這個函式,起到一個臨時的代理器作用,這樣就拿到了資料。

JSONP 的後話

JSONP的這種實現方式不受同源策略的影響,相容性也很好;但是它之支援 GET 方式的清楚,只支援 HTTP 請求這種特殊的情況,對於兩個不同域之間兩個頁面的互相呼叫也是無能為力。

2. CORS

XMLHttpRequest 的同源策略看起來是如此的變態,即使是同一個公司的產品,也不可能完全在同一個域上面。還好,網路設計者在設計的時候考略到了這一點,可以在伺服器端進行一些定義,允許部分網路訪問。

CORS 的全稱是 Cross-Origin Resource Sharing,即跨域資源共享。他的原理就是使用自定義的 HTTP 頭部,讓伺服器與瀏覽器進行溝通,主要是通過設定響應頭的 Access-Control-Allow-Origin 來達到目的的。這樣,XMLHttpRequest 就能跨域了。

值得注意的是,正常情況下的 XMLHttpRequest 是隻傳送一次請求的,但是跨域問題下很可能是會發送兩次的請求(預傳送)。

更加詳細的內容可以參見:

https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS

CORS 的後話:

相比之下,CORS 就支援所有型別的 HTTP 請求了,但是在相容上面,往往一些老的瀏覽器並不支援 CORS。

Desktop:

瀏覽器 版本
Chrome 4
Firefox (Gecko) 3.5
Internet Explorer 8 (via XDomainReques) 10
Opera 12
Safari 4

Mobile:

裝置 版本
Android 2.1
Chrome for Android yes
Firefox Mobile (Gecko) yes
IE Mobile ?
Opera Mobile 12
Safari Mobile 3.2

3. window.name

window.name 在一個視窗(標籤)的生命週期之內是共享的,利用這點就可以傳輸一些資料。

除此之外,結合 iframe 還能實現更加強大的功能:

需要3個檔案: a/proxy/b

a.html

<script type="text/javascript">
    var state = 0, 
    iframe = document.createElement('iframe'),
    loadfn = function() {
        if (state === 1) {
            var data = iframe.contentWindow.name;    // 讀取資料
            alert(data);    //彈出'I was there!'
        } else if (state === 0) {
            state = 1;
            iframe.contentWindow.location = "http://a.com/proxy.html";    // 設定的代理檔案
        }  
    };
    iframe.src = 'http://b.com/b.html';
    if (iframe.attachEvent) {
        iframe.attachEvent('onload', loadfn);
    } else {
        iframe.onload  = loadfn;
    }
    document.body.appendChild(iframe);
</script>
b.html

<script type="text/javascript">
    window.name = 'I was there!';    // 這裡是要傳輸的資料,大小一般為2M,IE和firefox下可以大至32M左右
                                     // 資料格式可以自定義,如json、字串
</script>

proxy 是一個代理檔案,空的就可以,需要和 a 在同一域下

4. document.domain

在不同的子域 + iframe互動的時候,獲取到另外一個 iframe 的 window物件是沒有問題的,但是獲取到的這個window的方法和屬性大多數都是不能使用的。

這種現象可以藉助document.domain 來解決。

example.com

<iframe id='i' src="1.example.com" onload="do()"></iframe>
<script>
  document.domain = 'example.com';
  document.getElementById("i").contentWindow;
</script>
1.example.com

<script>
  document.domain = 'example.com';  
</script>

這樣,就可以解決問題了。值得注意的是:document.domain 的設定是有限制的,只能設定為頁面本身或者更高一級的域名。

document.domain的後話:

利用這種方法是極其方便的,但是如果一個網站被攻擊之後另外一個網站很可能會引起安全漏洞。

5.location.hash

這種方法可以把資料的變化顯示在 url 的 hash 裡面。但是由於 chrome 和 IE 不允許修改parent.location.hash 的值,所以需要再加一層。

a.html 和 b.html 進行資料交換。

a.html

function startRequest(){
    var ifr = document.createElement('iframe');
    ifr.style.display = 'none';
    ifr.src = 'http://2.com/b.html#paramdo';
    document.body.appendChild(ifr);
}

function checkHash() {
    try {
        var data = location.hash ? location.hash.substring(1) : '';
        if (console.log) {
            console.log('Now the data is '+data);
        }
    } catch(e) {};
}
setInterval(checkHash, 2000);
b.html

//模擬一個簡單的引數處理操作
switch(location.hash){
    case '#paramdo':
        callBack();
        break;
    case '#paramset':
        //do something……
        break;
}

function callBack(){
    try {
        parent.location.hash = 'somedata';
    } catch (e) {
        // ie、chrome的安全機制無法修改parent.location.hash,
        // 所以要利用一箇中間域下的代理iframe
        var ifrproxy = document.createElement('iframe');
        ifrproxy.style.display = 'none';
        ifrproxy.src = 'http://3.com/c.html#somedata';    // 注意該檔案在"a.com"域下
        document.body.appendChild(ifrproxy);
    }
}
c.html

//因為parent.parent和自身屬於同一個域,所以可以改變其location.hash的值
parent.parent.location.hash = self.location.hash.substring(1);

這樣,利用中間的 c 層就可以用 hash 達到 a 與 b 的互動了。

6.window.postMessage()

這個方法是 HTML5 的一個新特性,可以用來向其他所有的window物件傳送訊息。需要注意的是我們必須要保證所有的指令碼執行完才傳送MessageEvent,如果在函式執行的過程中呼叫了他,就會讓後面的函式超時無法執行。

https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

參考資料

http://www.cnblogs.com/rainman/archive/2011/02/20/1959325.html

http://www.cnblogs.com/rainman/archive/2011/02/21/1960044.html