跨域的九種解決方法

穆笙發表於2019-02-18

前言

首先什麼是跨域,簡單地理解就是因為JavaScript同源策略的限制,a.com 域名下的js無法操作b.com或是c.a.com域名下的物件

跨域的九種解決方法

什麼是同源策略?

       同源策略/SOP(Same origin policy)是一種約定,由Netscape公司1995年引入瀏覽器,它是瀏覽器最核心也最基本的安全功能,如果缺少了同源策略,瀏覽器很容易受到XSS、CSFR等攻擊。所謂同源是指"協議+域名+埠"三者相同,即便兩個不同的域名指向同一個ip地址,也非同源。

1、JSONP

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

JSONP實現跨域請求的原理簡單的說,就是動態建立<script>標籤,然後利用<script>的src 不受同源策略約束來跨域獲取資料。 

 JSONP 由兩部分組成:回撥函式和資料回撥函式是當響應到來時應該在頁面中呼叫的函式。回撥函式的名字一般是在請求中指定的。而資料就是傳入回撥函式中的 JSON 資料

動態建立<script>標籤,設定其src,回撥函式在src中設定:

var script = document.createElement("script");
script.src = "https://api.douban.com/v2/book/search?q=javascript&count=1&callback=handleResponse"
document.body.insertBefore(script, document.body.firstChild);
複製程式碼

在頁面中,返回的JSON作為引數傳入回撥函式中,我們通過回撥函式來來運算元據。

function handleResponse(response){
    // 對response資料進行操作程式碼
}
複製程式碼

2、postMessage

postMessage是html5引入的API,postMessage()方法允許來自不同源的指令碼採用非同步方式進行有效的通訊,可以實現跨文字文件,多視窗,跨域訊息傳遞.多用於視窗間資料通訊,這也使它成為跨域通訊的一種有效的解決方案.

傳送資料:

otherWindow.postMessage(message, targetOrigin, [transfer]);
複製程式碼

otherWindow

視窗的一個引用,比如iframe的contentWindow屬性,執行window.open返回的視窗物件,或者是命名過的或數值索引的window.frames.

message

要傳送到其他視窗的資料,它將會被[!結構化克隆演算法](https://developer.mozilla.org/en-US/docs/DOM/The_structured_clone_algorithm)序列化.這意味著你可以不受什麼限制的將資料物件安全的傳送給目標視窗而無需自己序列化.

targetOrigin

通過視窗的origin屬性來指定哪些視窗能接收到訊息事件,指定後只有對應origin下的視窗才可以接收到訊息,設定為萬用字元"*"表示可以傳送到任何視窗,但通常處於安全性考慮不建議這麼做.如果想要傳送到與當前視窗同源的視窗,可設定為"/"

transfer | 可選屬性

是一串和message同時傳遞的**Transferable**物件,這些物件的所有權將被轉移給訊息的接收方,而傳送一方將不再保有所有權.

接收資料: 監聽message事件的發生

window.addEventListener("message", receiveMessage, false) ;
function receiveMessage(event) {
     var origin= event.origin;
     console.log(event);
}複製程式碼

event物件的列印結果截圖如下:

跨域的九種解決方法

event物件的四個屬性

  • data : 指的是從其他視窗傳送過來的訊息物件;
  • type: 指的是傳送訊息的型別;
  • source: 指的是傳送訊息的視窗物件;
  • origin: 指的是傳送訊息的視窗的源

 3、跨域資源共享(CORS)

CORS 需要瀏覽器和後端同時支援。IE 8 和 9 需要通過 XDomainRequest 來實現

瀏覽器會自動進行 CORS 通訊,實現 CORS 通訊的關鍵是後端。只要後端實現了 CORS,就實現了跨域。

服務端設定 Access-Control-Allow-Origin 就可以開啟 CORS。 該屬性表示哪些域名可以訪問資源,如果設定萬用字元則表示所有網站都可以訪問資源。

雖然設定 CORS 和前端沒什麼關係,但是通過這種方式解決跨域問題的話,會在傳送請求時出現兩種情況,分別為簡單請求複雜請求

(1)簡單請求

只要同時滿足以下兩大條件,就屬於簡單請求

條件1:使用下列方法之一:

  • GET
  • HEAD
  • POST

條件2:Content-Type 的值僅限於下列三者之一:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

請求中的任意 XMLHttpRequestUpload 物件均沒有註冊任何事件監聽器; XMLHttpRequestUpload 物件可以使用 XMLHttpRequest.upload 屬性訪問。

(2)非簡單請求

不符合以上條件的請求就肯定是複雜請求了。 複雜請求的CORS請求,會在正式通訊之前,增加一次HTTP查詢請求,稱為"預檢"請求,該請求是 option 方法的,通過該請求來知道服務端是否允許跨域請求。

我們用PUT向後臺請求時,屬於複雜請求,後臺需做如下配置:

// 允許哪個方法訪問我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 預檢的存活時間
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS請求不做任何處理
if (req.method === 'OPTIONS') {
  res.end() 
}
// 定義後臺返回的內容
app.put('/getData', function(req, res) {
  console.log(req.headers)
  res.end('我不愛你')
})
複製程式碼

接下來我們看下一個完整複雜請求的例子,並且介紹下CORS請求相關的欄位

// index.html
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiamen' // cookie不能跨域
xhr.withCredentials = true // 前端設定是否帶cookie
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'xiamen')
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
      console.log(xhr.response)
      //得到響應頭,後臺需設定Access-Control-Expose-Headers
      console.log(xhr.getResponseHeader('name'))
    }
  }
}
xhr.send()複製程式碼

//server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
複製程式碼

4、Node中介軟體代理

在前端網站開發過程中,網路請求指向nodejs提供的介面,nodejs服務端再發起請求指向跨域的伺服器,然後依次返回到前端頁面,這樣就完成了跨域的訪問,基本上就滿足了跨域訪問的問題了

前端程式碼

var xhr = new XMLHttpRequest();

// 瀏覽器是否讀寫cookie
xhr.withCredentials = true;

// 訪問http-proxy-middleware代理伺服器
xhr.open('get', 'http://www.127.0.0.1:3000/login?user=admin', true);
xhr.send();複製程式碼

express+ http-proxy-middleware

var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();

app.use('/', proxy({
    // 代理跨域介面
    target: 'http://www.127.0.0.1:8080',
    changeOrigin: true,

    // 修改響應頭資訊,實現跨域並允許帶cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.127.0.0.1');
        res.header('Access-Control-Allow-Credentials', 'true');
    },

    // 修改響應資訊中的cookie域名
    cookieDomainRewrite: 'www.127.0.0.1'  // 可以為false,表示不修改
}));

app.listen(3000);複製程式碼

Koa+Koa2-cors

var Koa = require('koa');
var cors = require('koa2-cors');

var app = new Koa();
app.use(cors({
  origin: function(ctx) {
    if (ctx.url === '/') {
      return false;
    }
    return '*';
  },
  exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
  maxAge: 5,
  credentials: true,
  allowMethods: ['GET', 'POST', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}));
app.listen(3000);複製程式碼

5、WebSocket協議跨域

WebSocket protocol是HTML5一種新的協議。它實現了瀏覽器與伺服器全雙工通訊,同時允許跨域通訊,是server push技術的一種很好的實現。
原生WebSocket API使用起來不太方便,我們使用Socket.io,它很好地封裝了webSocket介面,提供了更簡單、靈活的介面,也對不支援webSocket的瀏覽器提供了向下相容。

前端

<div>user input:<input type="text"></div>
<script src="./socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 連線成功處理
socket.on('connect', function() {
    // 監聽服務端訊息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 監聽服務端關閉
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>複製程式碼

Nodejs socket後臺

var http = require('http');
var socket = require('socket.io');

// 啟http服務
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

// 監聽socket連線
socket.listen(server).on('connection', function(client) {
    // 接收資訊
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 斷開處理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});複製程式碼

6、window.name + iframe

window.name屬性的獨特之處:name值在不同的頁面(甚至不同域名)載入後依舊存在,並且可以支援非常長的 name 值(2MB)。

其中a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000

 // a.html(http://localhost:3000/b.html)
  <iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
  <script>
    let first = true
    // onload事件會觸發2次,第1次載入跨域頁,並留存資料於window.name
    function load() {
      if(first){
      // 第1次onload(跨域頁)成功後,切換到同域代理頁面
        let iframe = document.getElementById('iframe');
        iframe.src = 'http://localhost:3000/b.html';
        first = false;
      }else{
      // 第2次onload(同域b.html頁)成功後,讀取同域window.name中資料
        console.log(iframe.contentWindow.name);
      }
    }
  </script>複製程式碼

b.html為中間代理頁,與a.html同域,內容為空。

 // c.html(http://localhost:4000/c.html)
     // c.html(http://localhost:4000/c.html)
  <script>
    window.name = '我要跨域'  
  </script>
複製程式碼

通過iframe的src屬性由外域轉向本地域,跨域資料即由iframe的window.name從外域傳遞到本地域。這個就巧妙地繞過了瀏覽器的跨域訪問限制,但同時它又是安全操作。

7、location.hash + iframe

實現原理: a欲與b跨域相互通訊,通過中間頁c來實現。 三個頁面,不同域之間利用iframe的location.hash傳值,相同域之間直接js訪問來通訊。

具體實現:A域:a.html -> B域:b.html -> A域:c.html,a與b不同域只能通過hash值單向通訊,b與c也不同域也只能單向通訊,但c與a同域,所以c可通過parent.parent訪問a頁面所有物件。

// a.html

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html傳hashsetTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
    
    // 開放給同域c.html的回撥方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

複製程式碼
// b.html<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 監聽a.html傳來的hash值,再傳給c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>複製程式碼
// c.html
<script>
    // 監聽b.html傳來的hash值
    window.onhashchange = function () {
        // 再通過操作同域a.html的js回撥,將結果傳回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>複製程式碼

8、document.domain + iframe

該方式只能用於二級域名相同的情況下,比如 a.test.comb.test.com 適用於該方式。 只需要給頁面新增 document.domain ='test.com' 表示二級域名都相同就可以實現跨域。

實現原理:兩個頁面都通過js強制設定document.domain為基礎主域,就實現了同域。

我們看個例子:頁面a.zf1.cn:3000/a.html獲取頁面b.zf1.cn:3000/b.html中a的值

// a.html
<body>
 helloa
  <iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
  <script>
    document.domain = 'zf1.cn'
    function load() {
      console.log(frame.contentWindow.a);
    }
  </script>
</body>複製程式碼

// b.html
<body>
   hellob
   <script>
     document.domain = 'zf1.cn'
     var a = 100;
   </script>
</body>
複製程式碼

9、nginx代理跨域

1、 nginx配置解決iconfont跨域

瀏覽器跨域訪問js、css、img等常規靜態資源被同源策略許可,但iconfont字型檔案(eot|otf|ttf|woff|svg)例外,此時可在nginx的靜態資源伺服器中加入以下配置。

location / {
  add_header Access-Control-Allow-Origin *;
}複製程式碼
2、 nginx反向代理介面跨域

跨域原理: 同源策略是瀏覽器的安全策略,不是HTTP協議的一部分。伺服器端呼叫HTTP介面只是使用HTTP協議,不會執行JS指令碼,不需要同源策略,也就不存在跨越問題。

實現思路:通過nginx配置一個代理伺服器(域名與domain1相同,埠不同)做跳板機,反向代理訪問domain2介面,並且可以順便修改cookie中domain資訊,方便當前域cookie寫入,實現跨域登入。

nginx具體配置

#proxy伺服器
server {
    listen       81;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie裡域名
        index  index.html index.htm;

        # 當用webpack-dev-server等中介軟體代理介面訪問nignx時,此時無瀏覽器參與,故沒有同源限制,下面的跨域配置可不啟用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #當前端只跨域不帶cookie時,可為*
        add_header Access-Control-Allow-Credentials true;
    }
}複製程式碼

 前端程式碼

var xhr = new XMLHttpRequest();

// 前端開關:瀏覽器是否讀寫cookie
xhr.withCredentials = true;

// 訪問nginx中的代理伺服器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();複製程式碼

Nodejs後臺

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var params = qs.parse(req.url.substring(2));

    // 向前臺寫cookie
    res.writeHead(200, {
        'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  // HttpOnly:指令碼無法讀取
    });

    res.write(JSON.stringify(params));
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');複製程式碼

總結

推薦使用nginx解決跨域問題,有以下優點:

  • 相容性極佳,能適用所有的瀏覽器
  • 成本低,服務端無需額外配置,前端程式碼也無需修改
  • 節約伺服器的效能
  • 能攜帶Session,無需額外配置cookie等驗證資訊

參考

跨域資源共享 CORS 詳解

window.postMessage

通過nginx反向代理解決前端訪問的跨域問題

前端跨域總結


相關文章