總結:前端常見的跨域解決方案

1、形成跨域的兩種策略

  瀏覽器的同源策略會致使跨域,這裏同源策略又分爲如下兩種:css

  DOM同源策略:禁止對不一樣源頁面DOM進行操做。這裏主要場景是iframe跨域的狀況,不一樣域名的iframe是限制互相訪問的。html

  XmlHttpRequest同源策略:禁止使用XHR對象向不一樣源的服務器地址發起HTTP請求。 只要協議、域名、端口有任何一個不一樣,都被看成是不一樣的域,之間的請求就是跨域操做。前端

2、爲何要有跨域限制

  瞭解完跨域以後,想必你們都會有這麼一個思考,爲何要有跨域的限制,瀏覽器這麼作是出於何種緣由呢。其實仔細想想就會明白,跨域限制主要是爲了安全考慮。vue

  常見的跨域場景:html5

URL                                      說明 是否容許通訊 http://www.domain.com/a.js http://www.domain.com/b.js 同一域名,不一樣文件或路徑 容許 http://www.domain.com/lab/c.js  http://www.domain.com:8000/a.js http://www.domain.com/b.js 同一域名,不一樣端口 不容許  http://www.domain.com/a.js https://www.domain.com/b.js 同一域名,不一樣協議 不容許  http://www.domain.com/a.js http://192.168.4.12/b.js 域名和域名對應相同ip 不容許  http://www.domain.com/a.js http://x.domain.com/b.js 主域相同,子域不一樣 不容許 http://domain.com/c.js  http://www.domain1.com/a.js http://www.domain2.com/b.js 不一樣域名 不容許

(1)AJAX同源策略主要用來防止CSRF攻擊。若是沒有AJAX同源策略,至關危險,咱們發起的每一次HTTP請求都會帶上請求地址對應的cookie,那麼能夠作以下攻擊:java

  一、用戶登陸了本身的銀行頁面http://mybank.com,http://mybank.com向用戶的cookie中添加用戶標識node

  二、用戶瀏覽了惡意頁面 http://evil.com。執行了頁面中的惡意AJAX請求代碼。webpack

  三、http://evil.com向http://mybank.com發起AJAXHTTP請求,請求會默認把http://mybank.com對應cookie也同時發送過去。nginx

  四、銀行頁面從發送的cookie中提取用戶標識,驗證用戶無誤,response中返回請求數據。此時數據就泄露了。web

  五、並且因爲Ajax在後臺執行,用戶沒法感知這一過程。

(2)DOM同源策略也同樣,若是iframe之間能夠跨域訪問,能夠這樣攻擊:

  一、作一個假網站,裏面用iframe嵌套一個銀行網站 http://mybank.com

  二、把iframe寬高啥的調整到頁面所有,這樣用戶進來除了域名,別的部分和銀行的網站沒有任何差異。

  三、這時若是用戶輸入帳號密碼,咱們的主網站能夠跨域訪問到http://mybank.com的dom節點,就能夠拿到用戶的輸入了,那麼就完成了一次攻擊。 因此說有了跨域跨域限制以後,咱們才能更安全的上網了。

3、跨域的解決方式

  (1)跨域資源共享

  CORS是一個W3C標準,全稱是」跨域資源共享」(Cross-origin resource sharing)。 對於這個方式,阮一峯老師總結的文章特別好,但願深刻了解的能夠看一下http://www.ruanyifeng.com/blog/2016/04/cors.html。

  這裏簡單的說一說大致流程。

  一、對於客戶端,咱們仍是正常使用xhr對象發送ajax請求。 惟一須要注意的是,咱們須要設置咱們的xhr屬性withCredentials爲true,否則的話,cookie是帶不過去的,設置: xhr.withCredentials = true;

  二、對於服務器端,須要在 response header中設置以下兩個字段: Access-Control-Allow-Origin: http://www.yourhost.com Access-Control-Allow-Credentials:true 這樣,咱們就能夠跨域請求接口了。

  (2)jsonp實現跨域

  基本原理就是經過動態建立script標籤,而後利用src屬性進行跨域。

  這麼說比較模糊,咱們來看個例子:

 1 // 定義一個fun函數
 2 function fun(fata) {
 3     console.log(data);
 4 };
 5 // 建立一個腳本,而且告訴後端回調函數名叫fun
 6 var body = document.getElementsByTagName('body')[0];
 7 var script = document.gerElement('script');
 8 script.type = 'text/javasctipt';
 9 script.src = 'demo.js?callback=fun';
10 body.appendChild(script);

  返回的js腳本,直接會執行。因此就執行了事先定義好的fun函數了,而且把數據傳入了進來。

  fun({"name": "name"})

  固然,這個只是一個原理演示,實際狀況下,咱們須要動態建立這個fun函數,而且在數據返回的時候銷燬它。 由於在實際使用的時候,咱們用的各類ajax庫,基本都包含了jsonp的封裝,不過咱們仍是要知道一下原理,否則就不知道爲何jsonp不能發post請求了~

  (3)document.domain + iframe跨域

  此方案僅限主域相同,子域不一樣的跨域應用場景。

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

  (1)父窗口:(http://www.domain.com/a.html)

1 <iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
2 <script>
3     document.domain = 'domain.com';
4     var user = 'admin';
5 </script>

  (2)子窗口:(http://child.domain.com/b.html)

1 <script>
2     document.domain = 'domain.com';
3     // 獲取父窗口中變量
4     alert('get js data from parent ---> ' + window.parent.user);
5 </script>

  (4)window.name + iframe跨域

  window對象擁有name屬性,它有一個特色:相同協議下,在一個頁面中,不隨URL的改變而改變

  經過window.name實現跨域也很簡單,iframe擁有contentWindow屬性,其指向該iframe的window對象的引用,若是在iframe的src指向的頁面中設置window.name值,那麼就能夠經過iframe.contentWindow.name就能夠拿到這個值了

1 var url = "http://funteas.com/lab/windowName";
2 var iframe = document.createElement('iframe')
3 iframe.onload = function(){
4     var data = iframe.contentWindow.name
5     console.log(data)
6 }
7 iframe.src = url
8 document.body.appendChild(iframe)

  然而,chrome會提示你跨域了! 而咱們已經知道window.name不隨URL的改變而改版,即onload時,已經獲取到了name,只不過由於不一樣源,當前頁面的腳本沒法拿到iframe.contentWindow.name,此時只須要把iframe.src改成同源便可

1 var url = "http://funteas.com/lab/windowName";
2 var iframe = document.createElement('iframe')
3 iframe.onload = function(){
4     iframe.src = 'favicon.ico';
5     var data = iframe.contentWindow.name
6     console.log(data)
7 }
8 iframe.src = url
9 document.body.appendChild(iframe)

  (5)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頁面全部對象。

  (1)a.html:(http://www.domain1.com/a.html)

 
 1 <iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
 2 <script>
 3     var iframe = document.getElementById('iframe');
 4 
 5     // 向b.html傳hash值
 6     setTimeout(function() {
 7         iframe.src = iframe.src + '#user=admin';
 8     }, 1000);
 9 
10     // 開放給同域c.html的回調方法
11     function onCallback(res) {
12         alert('data from c.html ---> ' + res);
13     }
14 </script>
 
 

  (2)b.html:(http://www.domain2.com/b.html)

 
1 <iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
2 <script>
3     var iframe = document.getElementById('iframe');
4 
5     // 監聽a.html傳來的hash值,再傳給c.html
6     window.onhashchange = function () {
7         iframe.src = iframe.src + location.hash;
8     };
9 </script>
 
 

  (3)c.html:(http://www.domain1.com/c.html)

 
1 <script>
2     // 監聽b.html傳來的hash值
3     window.onhashchange = function () {
4         // 再經過操做同域a.html的js回調,將結果傳回
5         window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
6     };
7 </script>
 

  (6)postMessage跨域

  postMessage是HTML5 XMLHttpRequest Level 2中的API,且是爲數很少能夠跨域操做的window屬性之一,它可用於解決如下方面的問題:

  (a)頁面和其打開的新窗口的數據傳遞(b)多窗口之間消息傳遞(c)頁面與嵌套的iframe消息傳遞

  用法:otherWindow.postMessage(message,targetOrigin);

  postMessage(data,origin)方法接受兩個參數:

  data: html5規範支持任意基本類型或可複製的對象,但部分瀏覽器只支持字符串,因此傳參時最好用JSON.stringify()序列化。

  origin: 協議+主機+端口號,也能夠設置爲"*",表示能夠傳遞給任意窗口,若是要指定和當前窗口同源的話設置爲"/"。

  (1)a.html:(http://www.domain1.com/a.html))

 1 <iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
 2 <script>       
 3     var iframe = document.getElementById('iframe');
 4     iframe.onload = function() {
 5         var data = {
 6             name: 'aym'
 7         };
 8         // 向domain2傳送跨域數據
 9         iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
10     };
11 
12     // 接受domain2返回數據
13     window.addEventListener('message', function(e) {
14         alert('data from domain2 ---> ' + e.data);
15     }, false);
16 </script>

  (2)b.html:(http://www.domain2.com/b.html)

 1 <script>
 2     // 接收domain1的數據
 3     window.addEventListener('message', function(e) {
 4         alert('data from domain1 ---> ' + e.data);
 5 
 6         var data = JSON.parse(e.data);
 7         if (data) {
 8             data.number = 16;
 9 
10             // 處理後再發回domain1
11             window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
12         }
13     }, false);
14 </script>

(7)WebSocket協議跨域

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

  (1)前端代碼

 1 <div>user input:<input type="text"></div>
 2 <script src="./socket.io.js"></script>
 3 <script>
 4 var socket = io('http://www.domain2.com:8080');
 5 
 6 // 鏈接成功處理
 7 socket.on('connect', function() {
 8     // 監聽服務端消息
 9     socket.on('message', function(msg) {
10         console.log('data from server: ---> ' + msg); 
11     });
12 
13     // 監聽服務端關閉
14     socket.on('disconnect', function() { 
15         console.log('Server socket has closed.'); 
16     });
17 });
18 
19 document.getElementsByTagName('input')[0].onblur = function() {
20     socket.send(this.value);
21 };
22 </script>

  (2)Nodejs socket後臺

 1 var http = require('http');
 2 var socket = require('socket.io');
 3 
 4 // 啓http服務
 5 var server = http.createServer(function(req, res) {
 6     res.writeHead(200, {
 7         'Content-type': 'text/html'
 8     });
 9     res.end();
10 });
11 
12 server.listen('8080');
13 console.log('Server is running at port 8080...');
14 
15 // 監聽socket鏈接
16 socket.listen(server).on('connection', function(client) {
17     // 接收信息
18     client.on('message', function(msg) {
19         client.send('hello:' + msg);
20         console.log('data from client: ---> ' + msg);
21     });
22 
23     // 斷開處理
24     client.on('disconnect', function() {
25         console.log('Client socket has closed.'); 
26     });
27 });

  (8)nginx代理跨域

  一、nginx配置解決iconfont跨域

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

1 location / {
2     add_header Access-Control-Allow-Origin *;
3   }

  二、nginx反向代理接口跨域

  跨域原理: 同源策略是瀏覽器的安全策略,不是HTTP協議的一部分。服務器端調用HTTP接口只是使用HTTP協議,不會執行JS腳本,不須要同源策略,也就不存在跨越問題。

  實現思路:經過nginx配置一個代理服務器(域名與domain1相同,端口不一樣)作跳板機,反向代理訪問domain2接口,而且能夠順便修改cookie中domain信息,方便當前域cookie寫入,實現跨域登陸。

  nginx具體配置:

 1 #proxy服務器
 2 server {
 3     listen       81;
 4     server_name  www.domain1.com;
 5 
 6     location / {
 7         proxy_pass   http://www.domain2.com:8080;  #反向代理
 8         proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie裏域名
 9         index  index.html index.htm;
10 
11         # 當用webpack-dev-server等中間件代理接口訪問nignx時,此時無瀏覽器參與,故沒有同源限制,下面的跨域配置可不啓用
12         add_header Access-Control-Allow-Origin http://www.domain1.com;  #當前端只跨域不帶cookie時,可爲*
13         add_header Access-Control-Allow-Credentials true;
14     }
15 }

  (1)前端代碼示例:

 
1 var xhr = new XMLHttpRequest();
2 
3 // 前端開關:瀏覽器是否讀寫cookie
4 xhr.withCredentials = true;
5 
6 // 訪問nginx中的代理服務器
7 xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
8 xhr.send();
 

  (2)Nodejs後臺示例:

 1 var http = require('http');
 2 var server = http.createServer();
 3 var qs = require('querystring');
 4 
 5 server.on('request', function(req, res) {
 6     var params = qs.parse(req.url.substring(2));
 7 
 8     // 向前臺寫cookie
 9     res.writeHead(200, {
10         'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:腳本沒法讀取
11     });
12 
13     res.write(JSON.stringify(params));
14     res.end();
15 });
16 
17 server.listen('8080');
18 console.log('Server is running at port 8080...');

(9)Nodejs中間件代理跨域

  node中間件實現跨域代理,原理大體與nginx相同,都是經過啓一個代理服務器,實現數據的轉發,也能夠經過設置cookieDomainRewrite參數修改響應頭中cookie中域名,實現當前域的cookie寫入,方便接口登陸認證。

  一、 非vue框架的跨域(2次跨域)

  利用node + express + http-proxy-middleware搭建一個proxy服務器。

  (1)前端代碼示例:

1 var xhr = new XMLHttpRequest();
2 
3 // 前端開關:瀏覽器是否讀寫cookie
4 xhr.withCredentials = true;
5 
6 // 訪問http-proxy-middleware代理服務器
7 xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
8 xhr.send();

  (2)中間件服務器:

 1 var express = require('express');
 2 var proxy = require('http-proxy-middleware');
 3 var app = express();
 4 
 5 app.use('/', proxy({
 6     // 代理跨域目標接口
 7     target: 'http://www.domain2.com:8080',
 8     changeOrigin: true,
 9 
10     // 修改響應頭信息,實現跨域並容許帶cookie
11     onProxyRes: function(proxyRes, req, res) {
12         res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
13         res.header('Access-Control-Allow-Credentials', 'true');
14     },
15 
16     // 修改響應信息中的cookie域名
17     cookieDomainRewrite: 'www.domain1.com'  // 能夠爲false,表示不修改
18 }));
19 
20 app.listen(3000);
21 console.log('Proxy server is listen at port 3000...');

  (3)Nodejs後臺示例:同nginx中

  二、 vue框架的跨域(1次跨域)

  利用node + webpack + webpack-dev-server代理接口跨域。在開發環境下,因爲vue渲染服務和接口代理服務都是webpack-dev-server同一個,因此頁面與代理接口之間再也不跨域,無須設置headers跨域信息了。

  webpack.config.js部分配置:

 1 module.exports = {
 2     entry: {},
 3     module: {},
 4     ...
 5     devServer: {
 6         historyApiFallback: true,
 7         proxy: [{
 8             context: '/login',
 9             target: 'http://www.domain2.com:8080',  // 代理跨域目標接口
10             changeOrigin: true,
11             cookieDomainRewrite: 'www.domain1.com'  // 能夠爲false,表示不修改
12         }],
13         noInfo: true
14     }
15 }

  總結:以上跨域詳解摘自不一樣的專欄整理而成,實際狀況下,通常用cors,jsonp等常見方法就能夠了。不過遇到了一些很是規狀況,咱們仍是須要知道有更多的方法能夠選擇的。

相關文章
相關標籤/搜索