跨域解決方案匯總


最新博客站點:歡迎來訪

一、同源與同源策略

       我們知道,同源指的是協議、域名、端口號全部相同。同源策略(Same Origin Policy)是一種約定,它是瀏覽器最核心也是最基本的安全功能,如果缺少了同源策略,則瀏覽器的正常功能都可能會受到影響。Web是構建在同源策略基礎之上的,瀏覽器只是針對同源策略的一種實現。同源策略是處於對用戶安全的考量的,如果缺少了同源的限制,那又怎么能夠確定別人的網站始終對你是友好的呢。針對非同源的情況制定了一些限制條件,1. 無法讀取不同源的cookie、LocalStorage、indexDB。2. 無法獲得不同源的DOM。3. 不能向不同源的服務器發送Ajax請求。

  在瀏覽器中,<script><img><iframe><link>等標簽都可以跨域加載資源,而不受同源策略的限制。事實上,在大多數情境下,我們經常是需要借用非同源來提供數據的,所以這就要用到跨域方面的技術了。

二、JSONP

       JSONP是指JSON Padding,JSONP是一種非官方跨域數據交換協議,由於script的src屬性可以跨域請求,所以JSONP利用的就是瀏覽器的這個原理,需要通信時,動態插入一個javascript標簽。請求的地址一般帶有一個callback參數,假設需要請求的地址為http://localhost:3000?callback=show,服務器返回的代碼一般是show()的JSON數據,而show函數恰恰是前端需要用的這個數據的函數。JSONP非常簡單易用,自動補全API利用的就是JSONP。

        一個簡單的例子:

var script = doxument.createElement("script");
script.setAttribute("type", "text/javascript");
script.src="http://example.com/ip?callback=handleResponse";
document.body.appendChild(script);

function handleResponse(data) {
    console.log('Your public IP address is: '+data.ip);
}

       JSONP解決跨域的本質:<script>標簽可以請求不同域名下的資源,即<script>請求不受瀏覽器同源策略的影響。上例中的script會向http://example.com/服務器發送請求,這個請求的url后面帶了個callback參數,是用來告訴服務器回調方法的方法名的。因為服務器收到請求后,會把相應的數據寫進handleResponse的參數,也就是服務器會返回如下的腳本:

handleResponse({
    "ip" : "8.8.8.8"
});

       這樣瀏覽器通過<script>下載的資源就是上面的腳本了,<script>下載完就會立即執行,也就是說http://example.com/ip?callback=handleResponse這個請求返回后就會立即執行上面的腳本代碼,而這個腳本代碼就是調用回調方法和拿到json數據了。

       我們再來看一個例子:

//請求代碼
function jsonp(callback) {
    var script = document.createElement("script");
        url = `https://localhost:3000?callback=${callback}`;
    script.setAttribute("src", url);
    document.querySelector("head").appendChild(script);
}
function show(data) {
    concole.log(`學生姓名為: ${data.name},年齡為: ${data.age},性別為: ${data.sex}`);
}
jsonp("show");
//響應代碼
const student = {
    name: "Knight",
    age: 19,
    sex: "male"
};
var callback = url.parse(req.url, true).query.callback;
res.writeHead(200,{
    "Content-Type": "application/json;charset=utf-8"
});
res.end(`${callback}(${JSON.stringify(student)})`);

  JSONP有一個很大問題,就是只能進行GET請求。

三、跨域源資源共享(CORS)

  CORS是W3C制定的跨站資源分享標准,可以讓AJAX實現跨域訪問,定義了在必須訪問跨域資源時瀏覽器與服務器該如何溝通。CORS背后的基本思想,就是使用自定義的HTTP頭部讓瀏覽器和服務器進行溝通,從而決定請求或響應應該成功還是失敗。

        比如一個簡單的使用GET或POST的請求,它沒有自定義的頭部,而主體內容是text/plain。在發送該請求時,需要給它附加一個額外的Origin頭部,其中包含請求頁面的源信息(協議、域名、端口號),以便服務器根據該頭部信息來決定是否給予響應。

Origin: http://www.example.com

  如果服務器認為這個請求可以接受,就在Access-Control-Allow-Origin頭部中發回相同的源信息(如果是公共資源,可以發“*”)。例如:

Access-Control-Allow-Origin: http://www.example.com

  如果沒有這個頭部信息或信息不匹配,瀏覽器就會駁回請求。正常情況下,瀏覽器會處理請求。此時,請求和響應都不包含Cookie信息。

  簡單請求的跨域:

  請求方式為GET或則POST;

  假若請求是POST的話,Content-Type必須為下列之一:

    application/x-www-form-urlencoded

    mutipart/form-data

       text/plain

  不含有自定義頭;

 對於簡單的跨域只進行一次http請求:

function ajaxPost(url, obj, header) {
    return new Promise((resolve, reject) => {
         var xhr=new XMLHttpRequest(),
              str = '' ;
              keys = Object.keys(obj);
         for(var i=0,len=keys.length;i<len;i++) {
             str +=`${keys[i]}=${obj[keys[i]]}&`;
         }
         str = str.substring(0, str.length - 1);
         xhr.open('post', url);
         xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
         if(header instanceof Object) {
             for(var k in header)
                  xhr.setRequestHeader(k, header[k]);
         }
         xhr.send(str);
         xhr.onreadystatechange = function() {
              if(xhr.readyState == 4) {
                  if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
                      resolve(xhr.responseText);
                  } else {
                      reject();
                  }
              }
         }
         
    });
}
ajaxPost("https://localhost:3000?page=cors", {
        name: "Knight",
        age: 19,
        sex: "male"
}).then((text) => {console.log(text);},
                ()=>{console.log("請求失敗");});
//后端處理
var postData = "";
req.on("data", (data) => {
    postData += data;
});
req.on("end", () => {
    postData = querystring.parse(postData);
    res.writeHead(200, {
        "Access-Control-Allow-Origin": "*",
        "Content-Type": "application/json;charset=utf-8"
    });
    if(postData.name === student.name && Number(postData.age) === student.age &&  postData.sex === student.sex) {
        res.end(`yeah! ${postData.name} is a good guy!`);
    } else {
        res.end("No! a bad guy!");
    }
});

對於非簡單請求來說,需要兩次http請求,其中在請求之前有一次預檢請求。

function ajaxPost(url, obj, header) {
    return new Promise((resolve, reject) => {
         var xhr=new XMLHttpRequest(),
              str = '' ;
              keys = Object.keys(obj);
         for(var i=0,len=keys.length;i<len;i++) {
             str +=`${keys[i]}=${obj[keys[i]]}&`;
         }
         str = str.substring(0, str.length - 1);
         xhr.open('post', url);
         xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
         if(header instanceof Object) {
             for(var k in header)
                  xhr.setRequestHeader(k, header[k]);
         }
         xhr.send(str);
         xhr.onreadystatechange = function() {
              if(xhr.readyState == 4) {
                  if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
                      resolve(xhr.responseText);
                  } else {
                      reject();
                  }
              }
         }
         
    });
}
ajaxPost("https://localhost:3000?page=cors", {
        name: "Knight",
        age: 19,
        sex: "male"
}, {"X-author": "Knight"}).then((text) => {console.log(text);},
                ()=>{console.log("Request Error!");});
//后端處理
var postData = "";
if(req.method == "OPTIONS") {
    res.writeHead(200, {
        "Access-Control-Max-Age": 3000,
        "Access-Control-Allow-Headers": "X-author",
        "Access-Control-Allow-Origin": "*",
        "Content-Type": "application/json;charset=utf-8"
    });
    res.end();
    return void 0;
}
req.on("data", (data) => {
    postData += data;
});
req.on("end", () => {
    postData = querystring.parse(postData);
    res.writeHead(200, {
        "Access-Control-Allow-Origin": "*",
        "Content-Type": "application/json;charset=utf-8"
    });
    if(postData.name === student.name && Number(postData.age) === student.age &&  postData.sex === student.sex) {
        res.end(`yeah! ${postData.name} is a good guy!`);
    } else {
        res.end("No! a bad guy!");
    }
});

  上面代碼中,兩個響應頭: Access-Control-Allow-Headers,用來指明在實際的請求中,可以使用那些自定義的http請求頭;Access-Control-Max-Age,用來指定此次預請求的結果的有效期,在有效期內則不會發出預請求,類似於緩存。

四、document.domain實現跨域

  可以將子域和主域的document.domian設為同一個主域來實現跨域。但前提條件是,這兩個域名必須屬於同一個基礎域名,所用的協議,端口都要一致,否則無法通過document.domain()來進行跨域。

        example 1:

        如果想要在你的http://www.knightboy.cn/a.html頁面里使用<iframe>調用另一個http://knightboy.cn/b.html頁面。這時候你想在a頁面里面獲取b頁面里的DOM,然后進行操作。然后你會發現你不能獲得b的DOM。document.getElementById("myIFrame").contentWindow.document或window.parent.document.body因為兩個窗口不同源而報錯。

        這時候你只需要在a頁面里和b頁面里把document.domian設置成相同的值就可以在兩個頁面里操作Dom了。

        example 2:

        如果你在http://www.knightboy.cn/a.html頁面里寫入了document.cookie = "test=hello world";你在http://knightboy.cn/b.html頁面是拿不到這個cookie的。

        原因在於,Cookie是服務器寫入瀏覽器的一小段信息,只有同源的網頁才能共享。但是,兩個網頁一級域名相同,二級域名不同,瀏覽器允許通過設置document.domain來共享Cookie。另外,服務器也可以在設置Cookie的時候,指定Cookie的所屬域名為一級域名。這樣的話,二級域名和三級域名不用做任何設置便可以讀取這個Cookie。

        有一點需要注意的是:document.domain雖然可以讀寫,但只能設置成自身或者是高一級的父域且主域必須相同。所以只能解決一級域名相同二級域名不同的跨域問題。還有就是document.domain只適用於Cookie和iframe窗口,LocalStorage和IndexDB無法通過這種方法跨域。

五、window.name跨域

        window對象有一個name屬性,該屬性有個特征:即在一個窗口(window)的生命周期內,窗口載入的所有頁面都是共享一個window.name的,每個頁面對window.name都有讀寫的權限,window.name是持久存在一個窗口載入過的所有頁面中的,並不會因新頁面的載入而進行重置。注意,window.name的值只能是字符串的形式,且這個字符串的大小最大能容許2M左右甚至更大的容量,因瀏覽器而異,但一般是夠用的。

        example 1:

  現在在一個瀏覽器的一個標簽頁里打開http://www.knightboy.cn/a.html頁面,你通過location.href = http://baidu.com/b.html,在同一個瀏覽器標簽頁里打開了不同域名下的頁面。這時候這兩個頁面你可以使用window.name來傳遞參數。因為window.name指的是瀏覽器窗口的名字,只要瀏覽器窗口相同,那么無論在哪個頁面里訪問都是一樣的。

        example 2:

  你的http://www.knightboy.cn/a.html頁面里使用<iframe>調用另一個http://baidu.com/b.html頁面。這時候你想在a頁面里獲取b頁面里的DOM,然后進行操作。結果會發現不能獲得b中的DOM。同樣會因為不同源而報錯,和上面提到的不同之處就是兩個頁面的一級域名也不相同。這時候document.domain就解決不了了。

        瀏覽器窗口有window.name屬性。這個屬性的最大特點就是,無論是否同源,只要在同一個窗口里,前一個網頁設置了這個屬性,后一個網頁可以讀取它。比如當在b頁面里設定window.name="hello",你再返回到a頁面,在a頁面訪問window.name,可以得到hello。這種方法的優點是,window.name容量很大,可以放置非常長的字符串;缺點是必須監聽子窗口window.name屬性的變化,影響網頁性能。

<!--a.html-->
<DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>跨域</title>
    <script>
        function getData() {//iframe載入后執行該函數
            var iframe = document.getElementById("proxy");
            iframe.onload=function() {//a.html與iframe屬於同源了,可以互相訪問
                var data = iframe.contentWindow.name;//獲取iframe里的window.name,也就是b.html頁面給它設置的數據
                alert(data);
        }
         iframe.src="data.html";//這里的data.html為隨便的一個頁面,目的是,使得a.html能訪問到iframe里的內容,也可設置成about:blank
    </script>    
<head>
<body>
    <iframe id="proxy" src="http://baidu.com/b.html" style="display:none" onload="getData()"></iframe>
</body>
</html>
<!--b.html-->
<script>
    window.name="this is some data you got from b.html";
<script>

六、window.postMessage方法跨域

   window.postMessage是一個安全的跨源通信方法。一般情況下,當且僅當執行腳本的頁面使用相同的協議(通常都是http)、相同的端口(http默認80,https默認443)和相同的host(兩個頁面的document.domain的值相同)時,才允許不同頁面上的腳本互相訪問。window.postMessage提供了一個可控的機制來安全地繞過這一限制,當其在正確使用的情況下。window.postMessage解決的不是瀏覽器與服務器之間的交互,解決的是瀏覽器不同窗口之間的通信問題,可以做的就是同步兩個網頁,當然這兩個網頁需要屬於同一個基礎域名。

  example 1:

  在a頁面打開了一個不同源的b頁面,你想要讓a和b這兩個頁面互相通信,例如,a要訪問b的LocalStorage。又或者,a頁面里的iframe的src是不同源的b頁面,你想要讓a和b兩個頁面互相通信,比如依舊是想通過a訪問b的LocalStorage。

  此時的解決辦法是:利用HTML5中新增的跨文檔通信API,這個API為window對象新增了一個window.postMessage方法,允許跨窗口通信,不論這兩個窗口是否同源。a就可以把它的Local Storage發給b,反之,依然可行。

  window.postMessage(message, targetOrigin, [transfer])三個參數分別表示為:

  • message是向目標窗口發送的數據;
  • targetOrigin屬性來指定哪些窗口能收到消息事件,其值可以是字符串“*”(表示無限制)或者一個URI(或者說是發送消息的目標域名);
  • transfer可選參數,是一串和message同時傳遞的Transferable對象,這些對象的所有權將被轉移給消息的接收方,而發送一方將不再保有所有權。

  另外就是,消息的接收方必須有監聽事件,否則發送消息時就會報錯。

  window.addEventListener("message", onmessage); onmessage接收到的message事件包含三個屬性:

  data: 從其他window中傳遞過來的數據。

  origin: 調用postMessage時消息發送窗口的origin。這個origin不能保證是該窗口的當前或未來origin,因為postMessage被調用后可能被導航到不同的位置。

  source: 對發送消息的窗口對象的引用;您可以使用此來在具有不同origin的兩個窗口間建立雙向通信。

//在a頁面執行
var
popUp = window.open('http://localhost:3000', 'title'); popUp.postMessage('Hello World!', 'http://localhost:3000');

  同時在http://localhost:3000的頁面里監聽message事件:

window.onload = function() {
    window.addEventListener('message', onmessage);
}
function onmessage(event) {
    if(event.origin == 'http://localhost:8080') {//"發送方a的域名"
        console.log(event.data);//"Hello World!"
    } else {
      console.log(event.data);//"Hello World!"
}
}

我們來看另外一個例子:

//發送端代碼
var domain = "https://localhost",
    index = 1,
    target = window.open(`${domain}/postmesssage-target.html`);
function send()  {
    setInterval(() => {
        target.postMessage(`第${index++}次數據發送`, domain);
    }, 1000);
}
window.addEventListener("message", (e)=>{
    if(e.data === 'ok')
        send();
    else
        console.log(e.data);
});

//接收端代碼
<head>
    <script>
        opener.postMessage("ok", opener.domain);
    </script>
</head>
<body>
<p id = "test"></p>
<script>
    var test = document.querySelector("#test");
    window.addEventListener("message", (e)=>{
        if(e.origin !== "http://localhost") {
            return void 0;
        }
        test.innerText = e.data;
    });
</script> 
</body>

  上面頁面中,接受頁面已經加載了,這時發送一個消息給發送端,發送端再開始向接收端發送數據。

七、片段識別符實現跨域

  片段識別符就是指URL的#號后面的部分。比如,http://example.com/x.html#fragment的#fragment。如果只是改變片段標識符,頁面不會重復刷新。父窗口和iframe的子窗口之間的通訊或者是window.open打開的子窗口之間的通訊。

  父窗口可以把信息,寫入子窗口的片段標識符。

var src= originURL + '#' + data;
document.getElementById('myIFrame').src = src;

  子窗口通過監聽hashchange事件得到通知。

window.onhashchange = checkMessage;
function checkMessage() {
    var message = window.location.hash;
    //...  
}

  同樣,子窗口也可以改變父窗口的片段標識符。

parent.location.href = target + '#' + hash;

  總之,父窗口改變子窗口的url的#號后面的部分,后者把要傳遞的的參數寫在#后面,子窗口監聽window.onhashchange事件,得到通知,讀取window.location.hash解析出有用的數據。同時子窗口也可以向父窗口傳遞數據。

 

參考:

跨域同源政策及其規避方法

跨域解決方案大全

跨域問題匯總


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM