跨域的九種解決方法

前言

首先什麼是跨域,簡單地理解就是由於JavaScript同源策略的限制,a.com 域名下的js沒法操做b.com或是c.a.com域名下的對象
javascript


什麼是同源策略?css

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

一、JSONP

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

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

 JSONP 由兩部分組成:回調函數和數據回調函數是當響應到來時應該在頁面中調用的函數。回調函數的名字通常是在請求中指定的。而數據就是傳入回調函數中的 JSON 數據java

動態建立<script>標籤,設置其src,回調函數在src中設置:
node

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做爲參數傳入回調函數中,咱們經過回調函數來來操做數據。
webpack

function handleResponse(response){
    // 對response數據進行操做代碼
}
複製代碼

二、postMessage

postMessage是html5引入的API,postMessage()方法容許來自不一樣源的腳本採用異步方式進行有效的通訊,能夠實現跨文本文檔,多窗口,跨域消息傳遞.多用於窗口間數據通訊,這也使它成爲跨域通訊的一種有效的解決方案.
nginx

發送數據:web

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: 指的是發送消息的窗口的源

 三、跨域資源共享(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);
複製代碼

四、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);複製代碼

五、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.'); 
    });
});複製代碼

六、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從外域傳遞到本地域。這個就巧妙地繞過了瀏覽器的跨域訪問限制,但同時它又是安全操做。

七、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>複製代碼

八、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>
複製代碼

九、nginx代理跨域

一、 nginx配置解決iconfont跨域

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

location / {
  add_header Access-Control-Allow-Origin *;
}複製代碼
二、 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反向代理解決前端訪問的跨域問題

前端跨域總結

相關文章
相關標籤/搜索