關於跨域以及跨域的實現方式

關於跨域

why?

爲何會有跨域?javascript

咱們得先了解下 ==同源策略(SOP, Same Origin Policy)==。css

瀏覽器出於安全方面的考慮,只能訪問與包含它的頁面位於同一個域中的資源,該策略爲通訊設置了「相同的協議、相同的域、相同的端口」這一限制。試圖訪問上述限制以外的資源,都會引起安全錯誤。這種安全策略能夠預防某些惡意行爲。html

簡而言之,前端

  1. 同協議 Same Protocol
  2. 同域名 Same Hostname
  3. 同端口號 Same Port

Same Protocol && Same Hostname && Same Porthtml5


What?

什麼是跨域?java

==跨域就是採起技術方案突破同源策略的限制,實現不一樣域之間交互(請求響應)。==node


How?

那麼如何實現跨域呢?
有如下幾種方法。git

==方法一==ajax

CORS (Cross-Origin Resource Sharing,跨域源資源共享),是一種ajax跨域請求資源的方式,支持現代瀏覽器,IE支持10以上,經過XMLHttpRequest實現Ajax通訊的一個主要限制就是同源策略。
CORS是W3C的一個工做草案,定義了在必須訪問跨境資源時,瀏覽器和服務器該如何溝通。CORS的基本思想,就時使用自定義的HTTP頭部讓瀏覽器和服務器進行溝通,從而決定請求或者響應應該成功仍是失敗。
實現思路:使用XMLHttpRequest發送請求時,瀏覽器會給該請求自動添加一個請求頭:Origin。服務器通過一系列處理,若是肯定請求來源頁面屬於白名單,則在響應頭部加入首部字段:Access-Control-Allow-Origin。瀏覽器比較請求頭部的Origin 和響應頭部的 Access-Control-Allow-Origin是否一致,一致的話,瀏覽器獲得響應數據。若是服務器沒有設置Access-Control-Allow-Origin 或者這個頭部源信息不匹配,瀏覽器就會駁回請求。json

模擬CORS的實現

步驟1.

如何假裝一個網站(在本地)?

1.編輯hosts文件

蘋果mac: 直接在git bash上輸入命令行操做便可 「sudo vi /etc.hosts」 ,或者下載一些圖形界面應用軟件直接修改。

Windows操做系統:

  1. win鍵(四個方塊的鍵)+ R = 彈開運行窗口
  2. 複製該文件路徑 c:windowssystem32driversetc
  3. 選中hosts文件,右鍵-屬性-安全-選擇組或用戶名(添加修改保存的權限的對象)- 編輯 - 再次選擇組或用戶名(添加修改保存的權限的對象 - 勾選權限(選項在此不表)
  4. 打開hosts文件,寫入 127.0.0.1 localhost;127.0.0.1 bai.com;127.0.0.1 google.com;能夠寫入你任何你想模擬的網站,按照這種對應關係格式便可, ip地址+域名。

步驟2.
所需工具
node.js && git bash(模擬服務器),一個簡單的html頁面裏面有個跨域請求的Ajax通訊。

<!-- 前端 -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Google</title>
    <style type="text/css">
        body>h1 {
            text-align: center;
        }
        h1 {
            margin: 0 auto;
        }

    </style>
</head>
<body>
    <h1>hello world</h1>
    <script type="text/javascript">
    //CORS的實現
    var xhr = new XMLHttpRequest();
    xhr.onload = function(){
        if( (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
            var responseData = xhr.responseText;
            console.log(responseData)
            //['NBA Break News','CBA Break News']
        }

    }
    xhr.open('GET', 'http://baidu.com:8080/getNews', true);
    xhr.send()

    </script>
</body>
</html>
//nodeJS模擬後端響應CORS的實現
var http = require('http');
var fs = require('fs');
var url = require('url');
var path = require('path');

http.createServer(function(req, res){

     var urlObj = url.parse(req.url, true)

    switch (urlObj.pathname){

    case '/getNews':
    var news = ['NBA Break News','CBA Break News']
    //CORS的實現
    res.setHeader('Access-Control-Allow-Origin','http://google.com:8080')
    /*res.setHeader('Access-Control-Allow-Origin','*')
    服務器設置公用接口
    */
    res.end(JSON.stringify(news));
    break;

    case '/' :
    if(urlObj.pathname == '/') {
        urlObj.pathname += 'index.html'
    }

    default: 
    var filePath = path.join(__dirname, urlObj.pathname);
    fs.readFile(filePath,'binary', function(error, fileContent){
        if(error){
            console.log('404')
            res.writeHeader(404, 'not found')
            res.end('<h1>404,not found</h1>')
        }else {
            res.write(fileContent, 'binary')
        }
    })
    }

}).listen(8080);

上面代碼就是CORS實現的過程。

  1. 在本地修改hosts文件,127.0.0.1 google.com, 頁面的url爲 http://google.com:8080。
  2. 在title爲google的頁面上添加一個ajax請求,該請求以get方法會向baiduServer的端口('http://baidu.com:8080/getNews')發送一個請求。
  3. 瀏覽器會給請求頭加上Origin: http://google.com:8080, Request URL: http://baidu.com:8080/getNews。
  4. baiduServer後端,響應頭添加首部字段。Access-Control-Allow-Origin: http://google.com:8080。 代表該服務器(baiduServer)接受請求並給予響應。
  5. 瀏覽器比較請求頭部的Origin 和響應頭部的 Access-Control-Allow-Origin是否一致,一致的話,瀏覽器獲得響應數據。若是服務器沒有設置Access-Control-Allow-Origin: http://google.com:8080 或者這個頭部源信息不匹配,瀏覽器就會駁回請求。

固然服務器也能夠設置公用接口, res.setHeader('Access-Control-Allow-Origin','*')

服務器設置公用接口, 任何人均可以使用該服務器這個端口的數據。

==方法二==

JSONP,是JSON with padding的簡寫(填充式JSON或參數式JSON)。

JSONP的原理,經過動態<script>元素,使用時能夠爲該元素的src屬性添加一個跨域URL, <script>元素的src有能力不受限制地從其餘域中,加載資源。
凡是擁有src這個屬性的元素均可以跨域,例如<script><img><iframe>。

JSONP和JSON看起來差很少,只不過是被包含在函數調用中的JSON。

JSONP由兩個部分組成:回調函數和數據。回調函數是當響應到來時應該在頁面中調用的函數。回調函數的名字通常是在請求中指定的,因此須要對應接口的後端配合才能實現。而數據就是傳入回調函數中的JSON數據。

模擬JSONP的實現

HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Google</title>
    <style type="text/css">
        body {
            text-align: center;
        }
        h1 {
            margin: 0 auto;
        }
        ul, li {
            list-style: none;
        }
    
    </style>

</head>
<body>
    
    <h1>hello world</h1>
    <ul class="news">

    </ul>
    
    <!--JSONP的代碼實現方法1-->

    <!-- <script type="text/javascript">
    
    function getResponseData(jsonData){
        document.write(jsonData[0] + ', ');
        document.write(jsonData[1]);
        //NBA Break News, CBA Break News
    
    }
    </script>
    <script src="http://baidu.com:8080/getNews?newsData=getResponseData">
    //getResponseData(["NBA Break News","CBA Break News"])
    </script> -->
    
    <!--JSONP的代碼實現方法2-->
    <script>
        
        var script = document.createElement('script');
        script.setAttribute('src', 'http://baidu.com:8080/getNews?newsData=getResponseData');
        $('body').appendChild(script);
        $('body').removeChild(script);

        

        function getResponseData(jsonData){
        var nodeStock = document.createDocumentFragment();
        for(var i = 0; i < jsonData.length; i++) {
            var newsNode = document.createElement('li');
            newsNode.innerText = jsonData[i];
            nodeStock.appendChild(newsNode)
        };

        $('.news').appendChild(nodeStock)
        // <li>NBA Break News</li>   <li>CBA Break News</li>
        };

        function $(selector) {
            return document.querySelector(selector);
        };

    </script>



</body>
</html>

nodeJS

var http = require('http');
var fs = require('fs');
var path = require('path');
var url = require('url');

http.createServer(function(req,res){
    var urlObj = url.parse(req.url, true);
    switch(urlObj.pathname) {
        case '/getNews' :
        var news = ['NBA Break News','CBA Break News'];
        res.setHeader('Content-Type','text/javascript; charset=utf-8');
        if(urlObj.query.newsData){
            var data = urlObj.query.newsData + '(' + JSON.stringify(news) + ')' ;
            res.end(data);
        } else {
            res.end(JSON.stringify(news))
        }
        break;

        case '/' :
        if(urlObj.pathname == '/') {
            urlObj.pathname +=  'index.html'
        }

        default:
            fs.readFile(path.join(__dirname, urlObj.pathname), function(error, data) {
                if(error) {
                    res.writeHeader(404, 'not found');
                    res.end('<h1>404, Not Found</h1>');
                } else {
                    res.end(data)
                }
            });

    };
}).listen(8080);

==方法三==

降域,主要應用場景是同一頁面下不一樣源的框架iframe請求

基於iframe實現的跨域,要求兩個域都必須屬於同一個基礎域, 好比 a.xx.com, b.xx.com,都有一個基礎域xx.com, 使用同一協議和端口,這樣在兩個頁面中同時添加documet.domain,就能夠實現父頁面操控子頁面(框架)。

關於document.domain, 用來獲得當前網頁的域名。在瀏覽器輸入URL,wwww.baidu.com。 http://wwww.baidu.com, document.domain 爲 "www.baidu.com"。 也能夠爲document.domain賦值, 不過有限制,就是前面提到的,只能賦值爲當前的域名或者基礎域名。
範例:

document.domain = "www.baidu.com" //successed 賦值成功, 當前域名。

document.domain = "baidu.com" // successed 賦值成功, 基礎域名。

可是下面的賦值會報錯(參數無效)。

"VM50:1 Uncaught DOMException: Failed to set the 'domain' property on 'Document': 'a.baidu.com' is not a suffix of 'www.baidu.com'.

at <anonymous>:1:17"。

範例
document.domain = "google.com" // fail, 參數無效

document.domain = "a.baidu.com" // fail, 參數無效

由於google.com 和 a.baidu.com不是當前的域名,也不是當前域名的基礎域名。
緣由: 瀏覽器爲了防止惡意修改document.domain來實現跨域偷取數據。

-- --
==模擬降域的實現==

錯誤範例:

hosts 文件設置 win10系統路徑爲 c:windowssystem32driversetchosts
127.0.0.1 a.com
127.0.0.1 b.com

a.com的一個網頁(a.html)裏面 利用iframe引入一個b.com裏的一個網頁(b.html )。在a.html裏面能夠看到b.html的內容,但不能用Javascript來操做它。
緣由: 這兩個頁面屬於不一樣的域,在操做以前,瀏覽器會檢測兩個頁面的域是否相等,相等則容許操做,不相等則報錯。
這個例子裏,不可能把a.html與b.html,利用JS改爲同一個域。緣由:兩個域的基礎域名不相等。

http://a.com:8080/a.html的控制檯(console), 輸入代碼window.frames[0].document.body //VM150:1 Uncaught DOMException: Blocked a frame with origin "http://a.com:8080" from accessing a cross-origin frame.

at <anonymous>:1:18
<!--a.com的一個網頁(a.html)裏面 利用iframe引入一個b.com裏的一個網頁(b.html ) -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>a.com:8080/a.html</title>
</head>
<body>
    <iframe src="http://b.com:8080/b.html" frameborder="1"></iframe>
</body>
</html>
<!-- b.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>b.com:8080/b.html</title>
</head>
<body>
    <h1>this is b.html </h1>
    <input type="text" placeholder="How are you? this is http://b.com:8080/b.html">
</body>
</html>
//nodeJS 
var http = require('http');
var fs = require('fs');
var path = require('path');
var url = require('url');

http.createServer(function(req,res){
    var urlObj = url.parse(req.url, true);
    switch(urlObj.pathname) {
        case '/getNews' :
        var news = ['NBA Break News','CBA Break News'];
        res.setHeader('Content-Type','text/javascript; charset=utf-8');
        if(urlObj.query.newsData){
            var data = urlObj.query.newsData + '(' + JSON.stringify(news) + ')' ;
            res.end(data);
        } else {
            res.end(JSON.stringify(news))
        }
        break;

        case '/' :
        if(urlObj.pathname == '/') {
            urlObj.pathname +=  'index.html'
        }

        default:
            var filePath = path.join(__dirname, 'static' ,urlObj.pathname);
            console.log(filePath)
            fs.readFile(filePath, function(error, data) {
                if(error) {
                    res.writeHeader(404, 'not found');
                    res.end('<h1>404, Not Found</h1>');
                } else {
                    res.end(data)
                }
            });

    };
}).listen(8080);

能夠把iframe的src改變爲"http://a.com:8080/b.html",這樣就能夠了,是不會有這個問題的,由於域相等。
控制檯不會報錯,可是這樣沒完成跨域。可使用html5中的postMessage來實現,針對基礎域不一樣的框架,這裏暫且不表, 在方法四,會用到這種方法。

window.frames[0].document.body

<body>​<h1>​this is b.html ​</h1>​<input type=​"text" placeholder=​"How are you? this is http:​/​/​b.com:​8080/​b.html">​</body>​

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>a.com:8080/a.html</title>
</head>
<body>
    <!-- <iframe src="http://b.com:8080/b.html" frameborder="1"></iframe> -->
    <iframe src="http://a.com:8080/b.html" frameborder="1"></iframe>
</body>
</html>

==正確範例:
降域的實現==

hosts文件設置

基礎域名相同

127.0.0.1 a.shawroc.com

127.0.0.1 b.shawroc.com

a.shawroc.com的裏面一個網頁(a.html)引入b.shawroc.com裏的一個網頁(b.html),a.shawroc.com仍是不能操做b.shawroc.com裏面的內容。
緣由:document.domain不同,a.shawroc.com vs b.shawroc.com。
可是兩個頁面的基礎域名是同樣的,經過JS,將兩個頁面的domain改爲同樣。
在a.html 和 b.html 裏都加入
<script type="text/javascript">
document.domian = shawroc.com
</script>

這樣在兩個頁面中同時添加document.domain, 就能夠實現父頁面操控子頁面(框架)。

控制檯
window.frames[0].document.body
//console輸出

<body>
    <h1>this is http://b.shawroc.com:8080/b.html </h1>
    <input type="text" placeholder="How are you? this is http://b.shawroc.com:8080/b.html">
    <script>
    document.domain = 'shawroc.com';
    </script>

</body>

代碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>a.shawroc.com:8080</title>
</head>
<body>
    <!-- <iframe src="http://b.com:8080/b.html" frameborder="1"></iframe> -->
    <iframe src="http://b.shawroc.com:8080/b.html" frameborder="1" height="400px" width="600px"></iframe>
    <script>
    document.domain = 'shawroc.com';
    </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>b.shawroc.com:8080/b.html</title>
</head>
<body>
    <h1>this is http://b.shawroc.com:8080/b.html </h1>
    <input type="text" placeholder="How are you? this is http://b.shawroc.com:8080/b.html">
    <script>
    document.domain = 'shawroc.com';
    </script>
</body>
</html>

==方法四==

html5的postMessage API

html5引入的postMessage()方法,容許來自不一樣源的腳本採用異步方式進行有限的通訊,能夠實現跨文本檔、多窗口、跨域消息傳遞。

postMessage(data, origin) 方法,接受兩個參數。

1.data:要傳遞的數據,html5規範中提到該參數能夠是JavaScript的任意基本類型或可複製的對象,然而並非全部瀏覽器都作到了這點兒,部分瀏覽器只能處理字符串參數,因此咱們在傳遞參數的時候須要使用JSON.stringify()方法對對象參數序列化,在低版本IE中引用json2.js能夠實現相似效果。

2.origin:字符串參數,指明目標窗口的源,協議+主機+端口號[+URL],URL會被忽略,因此能夠不寫,這個參數是爲了安全考慮,postMessage()方法只會將message傳遞給指定窗口,固然若是願意也能夠建參數設置爲"*",這樣能夠傳遞給任意窗口,若是要指定和當前窗口同源的話設置爲"/"。

範例

模擬postMessage的工做機制

改寫hosts文件

127.0.0.1 a.com

127.0.0.1 b.com

<!--a.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>a.com:8080</title>
</head>
<body>
    <div class="textMessageInACom">
        <input type="text" placeholder="http://a.com:8080/a.html">
    </div>
    <iframe src="http://b.com:8080/b.html" frameborder="1" height="400px" width="600px"></iframe>
    <script>

    $('.textMessageInACom input').addEventListener('input', function(){
        window.frames[0].postMessage(this.value, 'http://b.com:8080');
    })
    //步驟1 a.com:8080/a.html頁面下的input發生輸入事件時, 向目標窗口發一個MessageEvent事件<iframe src="http://b.com:8080/b.html">, MessageEvent.data能夠得到this.value的值。接下來切換到b.html頁面

    window.addEventListener('message', function(messageEvent){
        $('.textMessageInACom input').value = messageEvent.data
    })
    // 步驟4 監聽嵌套頁面b.com:8080的message事件,把b.com:8080的input.value(message.data)賦值給a.com:8080的input.value, 就能夠實現輸入內容的同步啦。
    
    function $(selector){
        return document.querySelector(selector);
    }

    </script>
</body>
</html>
<!--b.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>b.shawroc.com:8080/b.html</title>
</head>
<body>
    <h1>this is http://b.com:8080/b.html </h1>
    <input id="input" type="text" placeholder="How are you? this is http://b.com:8080/b.html">

</body>
<script>
 window.addEventListener('message', function(e){
     $('#input').value = e.data;
 })
 //步驟2, 在b.com:8080/b.html監聽message事件
 
 $('#input').addEventListener('input', function() {
     window.parent.postMessage(this.value, 'http://a.com:8080');
 })
 //步驟3,b.com的input發生輸入事件時,向嵌套頁面的父頁面window.parent, a.com:8080 postMessage,而後切回到a.html,


function $(selector){
        return document.querySelector(selector);
    }

</script>
    
</html>
//nodeJS  模擬後端
var http = require('http');
var fs = require('fs');
var path = require('path');
var url = require('url');

http.createServer(function(req,res){
    var urlObj = url.parse(req.url, true);
    switch(urlObj.pathname) {
        case '/getNews' :
        var news = ['NBA Break News','CBA Break News'];
        res.setHeader('Content-Type','text/javascript; charset=utf-8');
        if(urlObj.query.newsData){
            var data = urlObj.query.newsData + '(' + JSON.stringify(news) + ')' ;
            res.end(data);
        } else {
            res.end(JSON.stringify(news))
        }
        break;

        case '/' :
        if(urlObj.pathname == '/') {
            urlObj.pathname +=  'index.html'
        }

        default:
            var filePath = path.join(__dirname, 'postMessage' ,urlObj.pathname);
            fs.readFile(filePath, function(error, data) {
                if(error) {
                    res.writeHeader(404, 'not found');
                    res.end('<h1>404, Not Found</h1>');
                } else {
                    res.end(data)
                }
            });

    };
}).listen(8080);

解析代碼
步驟1, a.com:8080/a.html頁面下的input發生輸入事件時, 向目標窗口發一個MessageEvent事件<iframe src="http://b.com:8080/b.html">, MessageEvent.data能夠得到this.value的值。接下來切換到b.html頁面。

步驟2, 在b.com:8080/b.html監聽message事件,在這,就能夠實現 a.com:8080/a. html的input標籤輸入什麼,嵌入在 a.com:8080/a. html的iframe框架(b.com:8080/b.html)同步父頁面(a.com:8080/a. html)的輸入內容了。

步驟3,b.com:8080/b.html的input發生輸入事件時,向嵌套頁面的父頁面window.parent(a.com:8080 )postMessage,而後切回到a.html。

步驟4, 監聽嵌套頁面b.com:8080/b.html的messageEvent事件,把b.com:8080/b.html的input.value(message.data)賦值給a.com:8080的input.value, 實現輸入內容的雙向同步。

相關文章
相關標籤/搜索