一文 | 跨域及其解決方案

跨域及其解決方案

一文系列企圖經過一篇簡短的文章來梳理一個知識點,在雜碎的時間片斷中給本身帶來一點點提高。

爲何有跨域這個問題

簡單的說,是由於瀏覽器的同源策略javascript

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

若是兩個連接的協議、域名、端口都一致,那麼這兩個URL同源,不然不一樣源。html

假設A站點的連接爲https://news.a.com/www/index.html,B站點爲下列連接時其同源檢測以下表前端

URL 結果 緣由
https://news.a.com/www/.html 同源
https://news.a.com/mmm/index.html 同源
https://music.a.com/www/index.html 不一樣源 域名不一致
http://news.a.com/www/index.html 不一樣源 協議不一致
https://news.a.com:6666/www/index.html 不一樣源 端口不一致

同源策略分爲如下兩種:java

  • DOM同源策略,禁止對不一樣源的DOM元素進行操做。
  • XHR同源策略,禁止使用XHR對象向不一樣源的服務器發起請求。

舉個栗子,Jarry登錄了A站點準備購物,與此同時Jarry正在B站點網上衝浪。若是沒有同源策略,那麼B站點的腳本能夠輕輕鬆鬆的修改A站點的DOM結構或者向A站點的服務器發起不恰當的請求,致使存在安全隱患。nginx

IE瀏覽器有兩個意外web

  • 授信範圍:兩個相互之間高度互信的域名,不受同源策略的限制。
  • 端口:IE沒有將端口號加入到同源策略的組成部分之中。

什麼是跨域

當A站點與B站點不一樣源(只要協議、域名、端口三者其一不一致)時,A站點沒法獲取到B站點的服務或者數據,此時就產生了跨域express

跨域問題復現
上圖中,站點https://www.jarrychung.com企圖向不一樣源站點https://www.baidu.com發起GET請求,致使報錯。json

如何解決跨域

1 JSONP

須要服務端支持。api

JSON是一種經常使用的數據交換格式,而JSONP是JSON的一種使用模式,能夠經過這種模式來進行跨域獲取數據。重要的是,JSONP使用簡便,沒有兼容性問題。

同源策略下,不一樣源的站點沒法相互獲取到數據,但img/iframe/script標籤是個例外,這些標籤能夠經過src屬性獲取到不一樣源的服務器。當正常的請求一個JSON數據時,服務端會返回JSON格式的數據。當使用JSONP模式發送請求時,服務端返回的是一段可執行的JavaScript代碼。

// 舉個例子
// 正常請求服務器(https://news.a.com/news?id=666)時,數據以下:
{"id": 666,"text":"Jarry Chung"}
// JSONP模式請求(https://news.a.com/news?id=666?callback=fn)時,數據以下:
fn({"id": 666,"text":"Jarry Chung"})
// 而後使用回調函數即可以處理得到的數據

注意:JSONP只支持GET請求,服務端可能在JSONP響應中夾帶惡意代碼,判斷是否請求成功是困難的。

2 跨域資源共享(CORS)

須要服務端支持。

CORS常常被稱爲現代化版本的JSONP,可以發起全部種類的HTTP請求,以及擁有良好的錯誤處理。

跨源資源共享標準的工做原理是添加自定義的HTTP頭部,容許服務器描述容許使用Web瀏覽器讀取該信息的起源集。此外,對於可能對服務器數據產生反作用的HTTP請求方法,規範要求瀏覽器「預檢」請求,從而請求支持的方法。服務器使用HTTP OPTIONS請求方法,而後,在服務器「批准」後,使用實際的HTTP請求方法發送實際請求。服務器還能夠通知客戶端是否讓「憑據」(包括Cookie和HTTP認證數據)與請求一塊兒發送(翻譯自MDN)。

CORS的基本思想是使用自定義的HTTP頭部容許服務端和瀏覽器互相認識,從而讓服務端決定是否容許請求以及響應。

  • Access-Control-Allow-Origin:指定受權訪問的域
  • Access-Control-Allow-Methods:受權請求的方法(GET, POST, PUT, DELETE,OPTIONS等)
// 舉個例子
var exp = require('express');
var app = exp();
app.all('*', function(req, res, next) {
    // 設置容許的源
    res.header("Access-Control-Allow-Origin", "*");
    // 設置容許的HTTP請求方式
    res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
    res.header("Access-Control-Allow-Headers", "X-Requested-With");
    res.header("Content-Type", "application/json;charset=utf-8");
    next();
});
app.get('/user/:id/:pw', function(req, res) {
    res.send({id:req.params.id, password: req.params.pw});
});
app.listen(8000);

注意:是最爲推薦的方案,但古董級瀏覽器不支持CORS,如IE8如下的瀏覽器。

3 Nginx反向代理

主要在服務端上實現。

瀏覽器有同源策略,可是服務端沒有這個限制,所以能夠將請求發送給反向代理服務器,由服務器去請求數據,而後再將數據返回給前端。而前端幾乎不須要作任何處理。
Nginx解決方案示意圖

server {
    # 監聽80端口,能夠改爲其餘端口
    listen       80;
    # 當前服務的域名
    server_name  www.a.com;

    location / {
        proxy_pass http://www.a.com:81;
        proxy_redirect default;
    }
    # 添加訪問目錄爲/api的代理配置
    # 目錄爲/api開頭的請求將被轉發到82端口
    # 還記得嗎,端口不一樣也是不一樣源
    location /apis {
        rewrite  ^/apis/(.*)$ /$1 break;
        proxy_pass   http://www.a.com:82;
    }

4 window.name + iframe

在瀏覽器實現。

window.name的值是當前窗口的名字,要注意的是每一個iframe都有包裹它的window,而這個window是top window的子窗口,所以它天然也有window.name的屬性。window.name屬性,若是沒有被修改,那麼其值在不一樣的頁面(甚至不一樣域名)加載後依舊存在。另外,其值大小一般可達到2MB

其思想爲:在一個頁面中內嵌一個iframe標籤,由這個iframe進行獲取數據,將獲取到的數據賦值給window.name屬性,而後由頁面獲取該屬性的值。既巧妙的繞過了同源策略,同時該操做也是安全的。

但這裏有一個問題,即頁面和該頁面下的iframe src不一樣源的話,這個頁面是沒法操做iframe的,所以致使取不到name值。

name屬性的特性在這時候就很好用了,當前頁設置的值, 在頁面從新加載(非同域也能夠)後, 只要沒有被修改,值依然不變。可讓iframe的location指向爲與頁面相同的域,等iframe加載完後頁面就能夠取到name值了。

<body>
  <script type="text/javascript"> 
    // 代碼參考自:https://www.cnblogs.com/zichi/p/4620656.html
    function crossDomain(url, fn) {
      iframe = document.createElement('iframe');
      iframe.style.display = 'none';
      var state = 0;
    
      iframe.onload = function() {
        if(state === 1) {
          // 處理數據
          fn(iframe.contentWindow.name);
          // 清楚痕跡
          iframe.contentWindow.document.write('');
          iframe.contentWindow.close();
          document.body.removeChild(iframe);
        } else if(state === 0) {
          state = 1;
          // proxy.html爲與頁面同級的空白頁面
          iframe.contentWindow.location = 'http://localhost:81/proxy.html';
        }
      };

      iframe.src = url;
      document.body.appendChild(iframe);
    } 
    
    // 調用
    // 服務器地址
    var url = 'http://localhost:82/data.php';
    // 處理數據 data就是window.name的值(string)
    crossDomain(url, function(data) {
      var data = JSON.parse(iframe.contentWindow.name);
      console.log(data);
    });
  </script>
</body>

5 location.hash + iframe

主要在瀏覽器實現,須要服務端支持。

https://www.a.com/news#JarryChungIsSoCool這個URL中,location.hash的值爲JarryChungIsSoCool。由於改變hash值不會致使刷新頁面,所以能夠利用location.hash屬性來傳遞數據。缺點是數據容量以及類型受到限制、數據內容直接暴露出來。

其思想爲:若index頁面要獲取不一樣源服務器的數據,那麼動態插入一個iframe,將iframe的src屬性指向該服務器地址。因爲同源策略,此時top window和包裹這個iframe的子窗口還是沒法通訊的,所以改變子窗口的路徑,將數據看成hash值添加到改變後的路徑,而後就可以進行通訊(這一點與利用window.name跨域的原理幾乎一致),可以通訊後能夠在iframe中將頁面的地址改變,將數據加在index頁面地址的hash值上。index頁面監聽地址的hash值變化便可以取得數據。

6 document.domain + iframe

在瀏覽器實現。

該方案適用於主域名一致,子域名不一致的狀況。兩個頁面使用JavaScript將document.domain設置爲相同主域名,從而實現跨域。

<!-- 主頁面 a.html -->
<iframe src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'Jarry Chung';
</script>

<!-- 子頁面 b.html -->
<script>
    document.domain = 'domain.com';
    // 獲取父窗口中 user 變量
    alert(window.parent.user); // 'Jarry Chung'
</script>

7 postMessage()

在瀏覽器實現。

postMessage()是HTML5新增的方法,能夠實現跨文本檔通訊、多窗口通訊、跨域通訊。示意圖以下:
postMessage()解決方案示意圖
index.html將須要的數據請求發送給iframe或者另外一個頁面,iframe或另外一個頁面監聽到message後響應,取得數據後利用postMessage()接口將數據返回給index.html

postMessage()有兩個參數:

  • data:須要傳遞的數據,HTML5規範中該參數的類型能夠是JS的任意基本類型或者可複製的對象,但有部分瀏覽器只支持傳遞字符串,所以可能須要將該值處理成字符串後再傳遞。
  • origin:字符串參數,指明目標窗口的源,協議+主機+端口號[+URL],URL會被忽略,能夠不寫。postMessage()方法只會將message傳遞給指定窗口,固然若是願意也能夠把參數設置爲*,這樣能夠傳遞給任意窗口。

8 WebSocket協議

須要服務端支持。

WebSocket協議是HTML5一種新的協議,實現了瀏覽器與服務器全雙工通訊,同時容許跨域通信。用法以下:

var ws = new WebSocket('wss://echo.websocket.org');
// 鏈接打開後發送消息
ws.onopen = function (evt) {
    console.log('Connection open ...');
    ws.send('Hello WebSockets!');
};
// 接受消息後關閉鏈接
ws.onmessage = function (evt) {
    console.log('Received Message: ', evt.data);
    ws.close();
};
// 監聽關閉鏈接
ws.onclose = function (evt) {
    console.log('Connection closed.');
};

寫在最後

我的經驗表示在實戰中遇見跨域的狀況並很少,可是若是遇見了每每都會掉坑裏面。

做爲一線開發者,作好知識儲備是在須要的時候迅速解決問題的必要條件。

相關文章
相關標籤/搜索