前端跨域方法論

前言

本着學習和總結的態度寫的技術輸出,文中有任何錯誤和問題,請大家指出。更多的技術輸出可以查看我的 github博客

整理了一些前端的學習資源,希望能夠幫助到有需要的人,地址: 學習資源彙總

跨域

跨域指的是協議(protocol ),域名(host),端口號(post)都不相同的資源之間嘗試着進行交互通信,而由於受瀏覽器同源策略的限制,無法正常進行交互通信。

最常見的實際場景就是在項目開發過程中,會存在請求第三方其他域下的資源,例如:使用地圖 API 的時候,設置密鑰的時候需要設置白名單才能正常使用地圖 API。

image

使用 AJAX 請求第三方不同域下的數據資源的時候,如果不處理跨域問題,便不能成功發送 HTTP 請求,且瀏覽器會發出錯誤警告。

同源策略

MDN 解釋: 同源策略限制了從同一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。

瀏覽器的同源策略的目的就是爲了防止 XSS,CSRF 等惡意攻擊。

同源策略的交互方式有三種:

  • 通常允許跨域寫操作,例如鏈接,重定向等。
  • 通常允許跨域嵌套資源,例如 img,script 標籤等。
  • 通常不允許跨域讀操作。

跨域場景

只有資源之間的協議,域名和端口號都相同,纔是同一個源。

下面是關於同源以及不同源之間的跨域描述。

URL 說明 是否允許通訊
http://www.demo.com/a.html
http://www.demo.com/b.html
http://www.demo.com/c.html
同一域名 允許
http://www.demo.com/news/a.html
http://www.demo.com/center/b....
http://www.demo.com/server/c....
同一域名下的不同文件夾 允許
http://www.demo.com/a.html
http://www.demo.com:80/b.html
不同端口號 不允許
http://www.demo.com/a.html
https://www.demo.com/b.html
不同協議 不允許
http://www.demo.com/a.html
http://www.test.com/b.html
不同域名 不允許
http://www.demo.com/a.html
http://test.demo.com/b.html
主域相同,子域不同 不允許

跨域解決方案

1. JSONP

由於瀏覽器同源策略是允許 script 標籤這樣的跨域資源嵌套的,所以 script 標籤的資源不受同源策略的限制。

JSONP 的解決方案就是通過 script 標籤進行跨域請求。

  • 前端設置好回調函數,並把回調函數當做請求 url 攜帶的參數。
  • 後端接受到請求之後,返回回調函數名和需要的數據。
  • 後端響應並返回數據後,返回的數據傳入到回調函數中並執行。
<!-- 通過原生使用 script 標籤 -->
<script>
    function jsonpCallback(data) {
        alert('獲取到的數據了,打開控制檯瞧瞧');
        console.log(data);
    }
</script>
<script src="http://127.0.0.1:3000?callback=jsonpCallback"></script>

也可以使用 AJAX GET 請求方式來跨域請求(axios GET 方式跨域同理)。

<!-- AJAX GET 請求 -->
<script>
    function jsonpCallback(data) {
        alert('獲取到的數據了,打開控制檯瞧瞧');
        console.log(data);
    }
    $.ajax({
        type: 'GET', // 必須是 GET 請求
        url: 'http://127.0.0.1:3000',
        dataType: 'jsonp', // 設置爲 jsonp 類型
        jsonpCallback: 'jsonpCallback' // 設置回調函數
    })
</script>

優缺點:

  • 兼容性好,低版本的 IE 也支持這種方式。
  • 只能支持 GET 方式的 HTTP 請求。
  • 只支持前後端數據通信這樣的 HTTP 請求,並不能解決不同域下的頁面之間的數據交互通信問題。

2. CORS

CORS 跨域資源共享允許在服務端進行相關設置後,可以進行跨域通信。

服務端未設置 CORS 跨域字段,服務端會拒絕請求並提示錯誤警告。

服務端設置 Access-Control-Allow-Origin 字段,值可以是具體的域名或者 '*' 通配符,配置好後就可以允許跨域請求數據。

<script>
    $.ajax({
    type: 'post',
    url: 'http://127.0.0.1:3000',
    success: function(res) {
        alert('獲取到的數據了,打開控制檯瞧瞧');
        console.log(res);
    }
})
</script>

服務端如何設置跨域字段? 後端語言設置跨域的方式都不一致,具體可參考後端語言本身的 API。

Node 端設置

res.writeHead(200, {
    'Access-Control-Allow-Origin': '*'
});

// 或者使用了 Express 這樣的框架
res.header("Access-Control-Allow-Origin", "*");

關於 CORS 的詳細,可以參考這篇筆記,CORS跨域資源共享

3. Server Proxy

通過服務端代理請求的方式也是解決瀏覽器跨域問題的方案。同源策略只是針對瀏覽器的安全策略,服務端並不受同源策略的限制,也就不存在跨域的問題。具體步驟如下:

  • 前端正常請求服務端提供的接口。比如請求接口:http://localhost:3000 。
  • 通過服務端設置代理髮送請求,請求到數據後再將需要的數據返回給前端。比如設置的代理請求接口是 https://cnodejs.org/api/v1/to... ,服務端代理將數據請求回來之後再將數據 http://localhost:3000 接口返回給前端。
// 服務端代理請求代碼
// 服務端只是簡單的通過正常的 HTTP 請求的方式來代理請求接口數據
// 或者也可以使用 proxy 模塊來代理,至於怎麼使用 proxy 模塊,待研究完善
var url = 'https://cnodejs.org/api/v1/topics';        
https.get(url, (resp) => {
    let data = "";
    resp.on('data', chunk => {
        data += chunk;
    });
    resp.on('end', () => {
        res.writeHead(200, {
            'Access-Control-Allow-Origin': '*',
            'Content-Type': 'application/json; charset=utf-8'
        });
        res.end(data);
    });
})

4. location.hash + iframe

location.hash + iframe 跨域通信的實現是這樣的:

  • 不同域的 a 頁面與 b 頁面進行通信,在 a 頁面中通過 iframe 嵌入 b 頁面,並給 iframe 的 src 添加一個 hash 值。
  • b 頁面接收到了 hash 值後,確定 a 頁面在嘗試着與自己通信,然後通過修改 parent.location.hash 的值,將要通信的數據傳遞給 a 頁面的 hash 值。
  • 但由於在 IE 和 Chrmoe 下不允許子頁面直接修改父頁面的 hash 值,所以需要一個代理頁面,通過與 a 頁面同域的 c 頁面來傳遞數據。
  • 同樣的在 b 頁面中通過 iframe 嵌入 c 頁面,將要傳遞的數據通過 iframe 的 src 鏈接的 hash 值傳遞給 c 頁面,由於 a 頁面與 c 頁面同域,c 頁面可以直接修改 a 頁面的 hash 值或者調用 a 頁面中的全局函數。

大致流程就是:

a 頁面代碼

<script>
    var iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = "http://localhost:8081/b.html#data";
    document.body.appendChild(iframe);

    function checkHash() {
        try {
            var data = location.hash ? location.hash.substring(1) : '';
            console.log('獲得到的數據是:', data);
        }catch(e) {}
    }
    window.addEventListener('hashchange', function(e) {
        console.log('監聽到hash的變化:', location.hash.substring(1));
    })
</script>

b 頁面代碼

<script>
     switch(location.hash) {
         case '#data':
         callback();
         break;
     }
    function callback() {
        var data = "testHash"
        try {
            parent.location.hash = data;
        }catch(e) {
            var ifrproxy = document.createElement('iframe');
            ifrproxy.style.display = 'none';
            ifrproxy.src = 'http://localhost:8080/c.html#' + data;
            document.body.appendChild(ifrproxy);
        }
    }
 </script>  

c 頁面代碼

<script>
    // 修改 a 頁面的 hash 值
    parent.parent.location.hash = self.location.hash.substring(1);
    // 調用 a 頁面的全局函數
    parent.parent.checkHash();
</script>

優缺點:

  • hash 傳遞的數據容量有限。
  • 數據直接暴露在 url 中。

5. document.domain + iframe

該方案只限於主域相同子域不同的資源跨域解決方案。

實際應用場景:

之前的項目開發中,經常碰到這樣的跨域問題,大致類似於在開發新產品的產品頁中,在沒有正式上線之前,一般都是上傳到內部的測試環境中,比如測試環境的域名是 test.admin.com/xxx/xxx,而項目的測試環境的域名是 consumer-test.admin.com/xxx/xxx 這樣的,產品頁是單獨分離部署上線,再通過 iframe 嵌套到項目中。在內部測試過程中,由於產品頁測試環境和項目測試環境主域相同而子域不同,且產品頁中需要用到項目中定義的全局公共資源,由於跨域問題,這些公共資源是獲取不到的。

這種場景的跨域解決方案就是利用 document.domain 設置。在產品頁和項目中將 document.domain 設置成相同域就可以實現跨域,嵌套的產品頁就可以訪問父頁面的公共資源了。需要注意的一點就是,document.domain 的設置是有限制的,只能設置成自身或者更高級的父域,且主域必須相同。

項目頁面

<iframe src="test.admin.com/xxx/xxx"></iframe>
<script>
    document.domain = 'admin.com';
</script>

產品頁

<script>
    // 設置之後就可獲取項目頁面中定義的公共資源了
    document.domain = 'admin.com';
</script>

6. window.name + iframe

window.name 指的是當前瀏覽器窗口的名稱,默認爲空字符串,每個窗口的 window.name 都是獨立的。iframe 嵌套的頁面中也有屬於自己的 window 對象,這個 window 是top window 的子窗口,也同樣擁有 window.name 的屬性。

window.name 的獨特之處在於當在頁面設置 window.name 的值,其實就是相當於給這個窗口設置了名稱,而後在這個窗口加載其他頁面(甚至不同域的頁面),window.name 的值依然存在(如果沒有重新設置那麼值不會變化),並且 window.name 的值支持比較大的存儲(2MB)。

例如: 隨便找個頁面打開控制檯,給當前窗口設置名稱。

window.name = 'test-name';

設置好之後可以在這個窗口下跳轉到其他頁面

window.location = 'https://www.baidu.com';

頁面跳轉到了百度首頁,但是 window.name 的值依然是之前設置的值,因爲是在一個窗口中跳轉的頁面,窗口名稱並不會被修改。

具體的跨域解決方式如下。

http://localhost:8080/a.html 與 http://localhost:8081/b.html 跨域通信,a 頁面通過 iframe 嵌套 b 頁面,b 頁面中設置好 window.name 的值,由於是不同域,a 頁面不能直接訪問到 b 頁面設置的 window.name 的值,需要一個與 a 頁面同域的中間頁來代理作爲 a 頁面與 b 頁面通信的橋樑。

a.html

<script>
    var data = null;
    var state = 0;
    var iframe = document.createElement('iframe');
    iframe.src = "http://localhost:8081/b.html";
    iframe.style.display = 'none';
    document.body.appendChild(iframe);
    
    // 第一次加載先加載 b.html,b.html 設置好了 window.name 的值
    // 而後加載 c.html,c.html 的 window.name 的值就是之前 b.html 設置的值
    // 同域的情況下,a.html 可以通過 iframe.contentWindow.name 獲取到 b.html 中 windoa.name 的值
    iframe.onload = function() {
        if(state === 0) {
            iframe.src = "http://localhost:8080/c.html";
            state = 1;
        }else if(state === 1) {
            data = iframe.contentWindow.name;
            console.log('收到數據:', data);
        }
    }
</script>

b.html

<script>
    window.name = '這是傳遞的數據';
</script>

中間代理頁,只需要跟 a 頁面保持同域就可以了,例如: http://localhost:8080/c.html 。

7. window.postMessage

postMessage 是 HTML5 的新特性,用於頁面之間跨域通信。

postMessage 方法接受兩個必要的參數:

  • message: 需要傳遞的數據。
  • targetOrigin: 數據傳遞的目標窗口域名,值可以是具體的域名或者 '*' 通配符。

a.html

<iframe src="http://localhost:8081/b.html" style='display: none;'></iframe>
<script>
    window.onload = function() {
        var targetOrigin = 'http://localhost:8081';
        var data = {
            name: '武林外傳',
            time: 2005,
            length: 81,
            address: '同福客棧'
        };
        // 向 b.html 發送消息
        window.frames[0].postMessage(data, targetOrigin);

        // 接收 b.html 發送的數據
        window.addEventListener('message', function(e) {
            console.log('b.html 發送來的消息:', e.data);
        })
    }
</script>

b.html

<script>
    var targetOrigin = 'http://localhost:8080';
    window.addEventListener('message', function(e) {
        if(e.source != window.parent) {
            return;
        }
        // 接收 a.html 發送的數據
        console.log('a.html 發送來的消息:', e.data);
        // 向 a.html 發送消息
        parent.postMessage('哈哈,我是b頁面,我收到你的消息了', targetOrigin);
    })
</script>

總結

  • 協議,域名,端口號不相同的資源之間相互通信,就會產生跨域問題。
  • 處於安全考慮,瀏覽器的同源策略限制了不同域之間相互通信。
  • JSONP,CORS,Server Proxy 跨域解決方式的應用場景都是用於前後端之間的數據通信,其他跨域解決方案主要是解決窗口頁面之間的數據通信。
  • JSONP 只支持 GET 方式的 HTTP 請求。
  • CORS 跨域資源請求需要後端支持。
  • Server Proxy 直接讓後端代理髮送請求。

後記

所有的跨域解決方案都有對應的 DEMO 實例,可在 DEMO 中查看。想要看運行效果,可以全局安裝 http-server 模塊。

npm install -g http-server

本着學習和總結的態度寫的技術輸出,文中有任何錯誤和問題,請大家指出。更多的技術輸出可以查看我的 github博客

整理了一些前端的學習資源,希望能夠幫助到有需要的人,地址: 學習資源彙總

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章