淺談WEB跨域的實現(前端向)

同源策略/SOP(Same origin policy)是一種約定,它是瀏覽器最核心也最基本的安全功能,若是缺乏了同源策略,瀏覽器很容易受到XSS、CSFR等攻擊(能夠參考個人這篇文章)。javascript

SOP要求兩個通信地址的協議域名端口號必須相同,不然兩個地址的通信將被瀏覽器視爲不安全的,並被block下來。好比「http頁面」和「https頁面」屬於不一樣協議;「qq.com」、「www.qq.com」、「a.qq.com」都屬於不一樣域名(或主機);「a.com」和「a.com:8000」屬於不一樣端口號。這三種狀況常規都是沒法直接進行通信的。html

咱們很容易模擬不一樣源的環境,用iframe來幫忙便可:前端

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>模擬跨域</title>
</head>
<body>
<iframe src="http://baidu.com"></iframe>
<script>
    window.frames[0].onload = function () {
        alert("1");
    }
</script>
</body>
</html>

上述代碼在chrome中會輸出blocking信息:java

即咱們沒法監聽百度首頁文檔onload的事件,由於top窗口跟iframe窗體是不一樣源的。node

現代瀏覽器的確在安全性上下了很多功夫,除了上述提到的默認禁止非同源頁面通信,還新增了CSP(Content Security Policy)報頭特性等安全限制功能。不過既然爲了用戶安全而關閉了一扇窗戶,天然也會爲開發者開啓一扇便利的窗戶,要突破SOP的限制,咱仍是有很多辦法和花樣的。ios

目錄web

CORSajax

XDRchrome

HTML5解決方案
   1. Cross-document messaging
   2. WebSocketnpm

JSONP

iframe形式
   1. document.domain
   2. location.hash
   3. window.name

其它形式
   1. 服務器代理
   2. flash socket

CORS

同域安全策略CORS(Cross-Origin Resource Sharing)是W3C在05年提出的跨域資源請求機制,它要求當前域(常規爲存放資源的服務器)在響應報頭添加Access-Control-Allow-Origin標籤,從而容許指定域的站點訪問當前域上的資源。咱們使用node/iojs來模擬一下(不懂node/iojs?不急,先看下個人入門文章):

服務器端:

require("http").createServer(function(req,res){
  //報頭添加Access-Control-Allow-Origin標籤,值爲特定的URL或「*」
  //「*」表示容許全部域訪問當前域
  res.setHeader("Access-Control-Allow-Origin","*");  
  res.end("OK");
}).listen(1234);

客戶端:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>CORS</title>
    <script src="jq.js"></script>
</head>
<body>
<div>catching data...</div>
<script>
    $.ajax({
        url:"http://127.0.0.1:1234/",
        success:function(data){
            $("div").text(data)
        }
    })
</script>
</body>
</html>

運行客戶端頁面後,便能看到div內容成功變爲服務端發來的「OK」,實現了兩個不一樣域的頁面間的通信。經過上述代碼咱們也發現,CORS主要是在服務端上的實現(也不外乎是添加一個報頭標籤),客戶端的實現跟常規的請求沒啥出入。

不過CORS默認只支持GET/POST這兩種http請求類型,若是要開啓PUT/DELETE之類的方式,須要在服務端在添加一個"Access-Control-Allow-Methods"報頭標籤:

服務端:

require("http").createServer(function(req,res){
  res.setHeader("Access-Control-Allow-Origin","http://127.0.0.1");
  res.setHeader(
    "Access-Control-Allow-Methods",
    "PUT, GET, POST, DELETE, HEAD, PATCH"
  );
  res.end(req.method+" "+req.url);
}).listen(1234);

XDR

惱人的IE8-是不支持上述的CORS滴,不過不走尋常路的巨硬在IE8開始引入了XDR(XDomainRequest)新特性(IE11已經再也不支持該特性),它實現了CORS的部分規範,只支持GET/POST形式的請求。另外在協議部分只支持 http 和 https 。

在服務器端,依舊要求在響應報頭添加"Access-Control-Allow-Methods"標籤(這點跟CORS一致)。

在客戶端,DR對象的使用方法與XHR對象很是類似,也是建立一個XDomainRequest的實例,調用open()方法,再調用send()方法。但與XHR對象的open()方法不一樣,XDR對象的open()方法只接收兩個參數:請求的類型和URL,由於全部XDR請求都是異步執行的,不能用它來建立同步請求。

請求返回以後,會觸發load事件,相應的數據也會保存在responseText屬性中,以下所示:

var xdr = new XDomainRequest();
xdr.onload = function() {
    alert(xdr.responseText);
};
xdr.onerror = function() {
    alert("一個錯誤發生了!");
};
xdr.open("get", "http://127.0.0.1:1234/");
xdr.send(null);

因爲XDR實在太過期,這裏不作太多介紹,瞭解下便可,更多細節請查閱msdn

HTML5解決方案

1. Cross-document messaging

在 Cross-document messaging 中,咱們可使用 postMessage 方法和 onmessage 事件來實現不一樣域之間的通訊,其中postMessage用於實時向接收信息的頁面發送消息,其語法爲:

  otherWindow.postMessage(message, targetOrigin);

otherWindow: 對接收信息頁面的window的引用。能夠是頁面中iframe的contentWindow屬性;window.open的返回值;經過name或下標從window.frames取到的值。
message: 所要發送的數據,string類型。
targetOrigin: 容許通訊的域的url,「*」表示不做限制。

咱們能夠在父頁面中嵌入不一樣域的子頁面(iframe實現,並且常規會把它隱藏掉),在子頁面調用 postMessage 方法向父頁面發送數據:

父頁面(http://localhost:10847/sop/a.html):

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>postMessage</title>
</head>
<body>
<iframe style="display:none;" id="ifr" src="http://127.0.0.1:10847/sop/b.html"></iframe>
<script type="text/javascript">
    window.addEventListener('message', function(event){
        // 經過origin屬性判斷消息來源地址
        if (event.origin == 'http://127.0.0.1:10847') {
            alert(event.data);    // 彈出從子頁面post過來的信息
        }
    }, false);
</script>
</body>
</html>

子頁面(http://127.0.0.1:10847/sop/b.html):

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>子頁面</title>
</head>
<body>
<script type="text/javascript">
    var ifr = window.parent;  //獲取父窗體
    var targetOrigin = 'http://localhost:10847';  // 若寫成 http://127.0.0.1:10847 則將沒法執行postMessage
    ifr.postMessage('這是傳遞給a.html的信息', targetOrigin);
</script>
</body>
</html>

執行以下:

關於 Cross-document messaging 的更多細節可參考這篇文檔

 

2. WebSocket

WebSocket protocol 是HTML5一種新的協議。它實現了瀏覽器與服務器全雙工通訊,同時容許跨域通信,是server push技術的一種很棒的實現。

咱們先簡單看下webSocket在客戶端上的api:

var ws = new WebSocket('ws://127.0.0.1:8080/url'); //新建一個WebSocket對象,注意服務器端的協議必須爲「ws://」或「wss://」,其中ws開頭是普通的websocket鏈接,wss是安全的websocket鏈接,相似於https。
ws.onopen = function() {
    // 鏈接被打開時調用
};
ws.onerror = function(e) {
    // 在出現錯誤時調用,例如在鏈接斷掉時
};
ws.onclose = function() {
    // 在鏈接被關閉時調用
};
ws.onmessage = function(msg) {
    // 在服務器端向客戶端發送消息時調用
    // msg.data包含了消息
};
// 這裏是如何給服務器端發送一些數據
ws.send('some data');
// 關閉套接口
ws.close();

服務端這塊咱們繼續用node/iojs來編寫,並使用socket.io模塊輔助,socket.io很好地封裝了webSocket接口,提供了更簡單、靈活的接口,也對不支持webSocket的瀏覽器提供了向下兼容(例如替換爲Flash Socket/Comet)。

咱們先寫服務端,首先咱們得在項目根目錄下使用npm命令安裝好socket.io模塊:

  npm install socket.io

接着新建服務端腳本(訪問地址是http://127.0.0.1:1234/)

var io = require('socket.io');
var server = require("http").createServer(function(req,res){
    res.writeHead(200, { 'Content-type': 'text/html'});
}).listen(1234);

io.listen(server).on('connection', function (client) {
    client.on('message', function (msg) { //監聽到信息處理
        console.log('Message Received: ', msg);
        client.send('服務器收到了信息:'+ msg);
    });
    client.on("disconnect", function() { //斷開處理
        console.log("Server has disconnected");
    })
});

客戶端頁面(http://localhost:10847/sop/a.html,注意使用了socket.io以後,接口跟原生的不太同樣了)

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>socket.io</title>
    <script src="jq.js"></script>
    <script src="https://cdn.socket.io/socket.io-1.3.4.js"></script>
</head>
<body>
Incoming Chat:
<ul></ul>
<br/>
<input type="text" />
<script>
    $(function () {
        var iosocket = io.connect('http://127.0.0.1:1234/'),
                $ul = $("ul"),
                $input = $("input");
        iosocket.on('connect', function() {  //接通處理
            $ul.append($('<li>連上啦</li>'));

            iosocket.on('message', function(message) {  //收到信息處理
                $ul.append($('<li></li>').text(message));
            });
            iosocket.on('disconnect', function() { //斷開處理
                $ul.append('<li>Disconnected</li>');
            });
        });

        $input.keypress(function (event) {
            if (event.which == 13) { //回車
                event.preventDefault();
                iosocket.send($input.val());
                $input.val('');
            }
        });
    });
</script>
</body>
</html>

客戶端頁面執行效果以下:

WebSocket能夠很好地擺脫無狀態的http鏈接,從而很好地處理鏈接斷開、數據錯誤的狀況,不過缺點是兼容性還不夠好,但咱可以使用上述的socket.io來向下兼容。

JSONP

這個實在用到爛大街了,提起跨域實現,其實最容易想到的就是它。JSONP(JSON with Padding)是JSON的一種「使用模式」,主要是利用script標籤不受同源策略限制的特性,向跨域的服務器請求並返回一段JSON數據。

常規先後端會約定好某個JSONP請求的callback名(好比隨便起個名字「abc」),服務端返回的JSON數據會被這個callback名包裹起來,進而方便服務器區分收到的請求,也方便客戶端區分其收到的響應數據。咱們能夠利用jQuery輕鬆實現JSONP:

客戶端(訪問地址http://localhost:10847/sop/a.html):

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>JSONP</title>
    <script src="jq.js"></script>
</head>
<body>
<div></div>
<script>
    $.ajax({
        url:'http://127.0.0.1:1234/',
        dataType:"jsonp", //告知jQ咱們走的JSONP形式
        jsonpCallback:"abc", //callback名
        success:function(data){
            console.log(data)
        }
    });
</script>
</body>
</html>

服務端(訪問地址http://127.0.0.1:1234/ ):

var http = require('http');
var urllib = require('url');

var data = {'name': 'vajoy', 'addr': 'shenzhen'}; 

http.createServer(function(req, res){
  res.writeHead(200, { 'Content-type': 'text/plain'});
  var params = urllib.parse(req.url, true);
  //console.log(params);
  if (params.query && params.query.callback) {
    //console.log(params.query.callback);
    var str =  params.query.callback + '(' + JSON.stringify(data) + ')';//jsonp
    res.end(str);
  } else {
    res.end(JSON.stringify(data));//普通的json
  }     
}).listen(1234)

客戶端執行結果:

不過JSONP始終是無狀態鏈接,不能獲悉鏈接狀態和錯誤事件,並且只能走GET的形式。

iframe形式

在好久之前的石器時代,對於不支持 XMLHttpRequest 的瀏覽器的最佳回溯方法之一就是使用IFRAME對象,固然常規只是用它來實現流模式的Comet,而不是解決跨域通訊的問題。

使用iframe跨域其實有點劍走偏鋒的既視感,也存在一些限制性。下面均來介紹下。

1. document.domain

該方法只適合主域相同但子域不一樣的狀況,好比 a.com 和 www.a.com,咱們只須要給這兩個頁面都加上一句 document.domain = 'a.com' ,就能夠在其中一個頁面嵌套另外一個頁面,而後進行窗體間的交互。

爲了方便模擬環境,咱們修改下hosts文件:

127.0.0.1     a.com
127.0.0.1     www.a.com

這樣咱們訪問 a.com 的時候便能映射到本地了。

頁面a.html(訪問地址http://a.com:8080/sop/a.html):

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>iframe</title>
    <script src="jq.js"></script>
</head>
<body>
<iframe src="http://www.a.com:8080/sop/b.html"></iframe>
<script>
    document.domain = 'a.com';
    $("iframe").load(function(){
        $(this).contents().find("div").text("OK")
    })
</script>
</body>
</html>

頁面b.html(訪問地址http://www.a.com:8080/sop/b.html):

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>b.html</title>
    <script src="jq.js"></script>
</head>
<body>
<div></div>
<script>
    document.domain = 'a.com';
</script>
</body>
</html>

這時候咱們訪問a.html會發現b.html裏的內容被成功修改:

 

2. location.hash

location.hash/url hash 是個好東西,在以前咱們曾利用avalon前端路由來實現簡單的SPA頁面(這篇文章),即是助力於location.hash。

利用url地址改變但不刷新頁面的特性(在url: http://a.com#hello 中的 '#hello' 就是location.hash,改變hash並不會致使頁面刷新,因此能夠利用hash值來進行數據傳遞)和iframe,咱們能夠實現跨域傳遞簡單信息。

不過這個實現略麻煩,常規咱們會想,在a.html下嵌套一個不一樣域的b.html,而後 a 和 b 互相修改彼此的hash值,也不斷監聽本身的hash值,從而實現咱們的需求。惋惜的是,大部分瀏覽器不容許修改不一樣域的父窗體的hash值(parent.location.hash),也就是說a雖能修改b的hash值,但反過來由b修改a的hash值卻不成立。

爲了解除該限制,咱們能夠在b頁面中增長一個和a同域的iframe(c.html)來作代理,這樣b能夠修改c,而c能夠修改a(即修改parent.parent.location.hash,別忘了a和c同域哦)。下面直接模擬這三個頁面,作到讓b向a傳輸信息(固然本質上是b向c,c再向a傳輸):

a.html(訪問地址http://a.com:8080/sop/a.html):

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>iframe</title>
    <script src="jq.js"></script>
</head>
<body>
<div></div>
<iframe src="http://www.a.com:8080/sop/b.html" style="display: none;"></iframe>
<script>
    var hash = "";
    function checkHash() {
        var data = location.hash ? location.hash.substring(1) : hash;
        if (hash !== data) {
            $("div").text('hash變化爲:' + data);
            hash = data;
        }
    }
    setInterval(checkHash, 2000);
</script>
</body>
</html>

b.html(訪問地址http://www.a.com:8080/sop/b.html):

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>b.html</title>
</head>
<body>
<script>
    try {  //有的瀏覽器(Firefox)仍是能夠直接操做parent.location.hash的
        parent.location.hash = 'a=1&b=2';
    } catch (e) {
        // ie、chrome的安全機制沒法修改parent.location.hash
        // 因此要利用一個代理iframe
        var ifrproxy = document.createElement('iframe');
        ifrproxy.style.display = 'none';
        ifrproxy.src = 'http://a.com:8080/sop/c.html#a=1&b=2'; //必須跟a.html同域
        document.body.appendChild(ifrproxy);
    }
</script>
</body>
</html>

c.html(訪問地址http://a.com:8080/sop/c.html):

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>c.html</title>
    <script src="jq.js"></script>
</head>
<body>
<script>
    //由於parent.parent和自身屬於同一個域,因此能夠改變其location.hash的值
    parent.parent.location.hash = self.location.hash.substring(1);
</script>
</body>
</html>

訪問a.html後,效果以下:

成功傳遞了數據「a=1&b=2」。該方法優勢是兼容較好,缺點卻顯而易見——可傳遞的數據類型、長度均受限,數據仍是直接顯示在url上的,不夠安全。另外其實現也較麻煩,還要搞setInterval不斷監聽,跟輪詢沒區別了。

 

3. window.name

window.name 的美妙之處在於,窗體的name值在頁面跳轉後依舊存在、保持原值(即便跳轉的頁面不一樣域),而且能夠支持很是長的 name 值(2MB)。

若是咱們在a頁面須要和不一樣域的b頁面通訊,咱們能夠如今a頁面嵌入b頁面,待b頁面有數據要傳遞時,把數據附加到b頁面窗口的window.name上,而後把窗口跳轉到一個和a頁面同域的c頁面,這樣a就能輕鬆獲取到內嵌窗體(地址已由跨域的b變爲同域的c)的window.name了(若是須要,獲取到數據後再把c跳轉到b,並重復循環前面的步驟,同時a頁面以setInterval的形式來達到輪詢的效果)。咱們繼續模擬這三個頁面:

a.html(訪問地址http://a.com:8080/sop/a.html):

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>window.name</title>
    <script src="jq.js"></script>
</head>
<body>
<div></div>
<iframe src="http://www.a.com:8080/sop/b.html" style="display: none;"></iframe>
<script>
    varifr = window.frames[0],
        loc = "",
        data = "";
    function checkData(){
        loc = ifr.location;
        if(loc.host){ //獲取到了,說明iframe已轉到同域的c頁面
            if(ifr.name !== data){ //說明有新數據
                data = ifr.name;
                $("div").text(JSON.parse(data).name);
                ifr.location = "http://www.a.com:8080/sop/b.html"; //數據收到後重回b頁面接收新數據
            }
        }else return;
    }

    setInterval(checkData,2000); //每2秒輪詢一次

</script>
</body>
</html>

b.html(訪問地址http://www.a.com:8080/sop/b.html):

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>b.html</title>
</head>
<body>
<script>
    window.name = '{"name":"vajoy","addr":"shenzhen"}';
    location = "http://a.com:8080/sop/c.html"; //跳轉到和a同域的c頁面
</script>
</body>
</html>

c.html頁面啥都不用寫,純粹一個空的html便可,畢竟只是一個代理頁面罷了。

咱們訪問a頁面,會成功收到來自不一樣域b的數據:

其它形式

1. 服務器

頁面直接向同域的服務端發請求,服務端進行跨域處理或爬蟲後,再把數據返回給客戶端頁面。依舊用node/iojs來模擬服務端,下面的代碼來自木的樹的文章

客戶端:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">
    <title>proxy_test</title>

    <script>
      var f = function(data){
        alert(data.name);
      }
      var xhr = new XMLHttpRequest();
      xhr.onload = function(){
        alert(xhr.responseText);
      };
      xhr.open('POST', 'http://localhost:8888/proxy?http://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer', true);
      xhr.send("f=json");
    </script>
  </head>
  
  <body>
  </body>
</html>
View Code

服務端:

var proxyUrl = "";
      if (req.url.indexOf('?') > -1) {
          proxyUrl = req.url.substr(req.url.indexOf('?') + 1);
          console.log(proxyUrl);
      }
      if (req.method === 'GET') {
          request.get(proxyUrl).pipe(res);
      } else if (req.method === 'POST') {
          var post = '';     //定義了一個post變量,用於暫存請求體的信息

        req.on('data', function(chunk){    //經過req的data事件監聽函數,每當接受到請求體的數據,就累加到post變量中
            post += chunk;
        });
    
        req.on('end', function(){    //在end事件觸發後,經過querystring.parse將post解析爲真正的POST請求格式,而後向客戶端返回。
            post = qs.parse(post);
            request({
                      method: 'POST',
                      url: proxyUrl,
                      form: post
                  }).pipe(res);
        });
      }
View Code

 

2. flash socket

其實在前面介紹socket.io的時候就有提到,在不兼容WebSocket的瀏覽器下,socket.io會以flash socket或Comet的形式來兼容,而flash socket是支持跨域通訊的形式,跟WebSocket同樣走的TCP/IP套接字協議。具體的實現可參考Adobe官方文檔,本文不贅述。

 

至此便介紹了這麼幾種經常使用的跨域通訊的實現方式,但願對你能有所幫助。

大過年的還這麼辛苦寫文章我也是瘋了,anyway,祝各位新春快樂,新的一年揚眉吐氣、萬事順利!

共勉~

donate

相關文章
相關標籤/搜索