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


一、造成跨域的兩種策略

  瀏覽器的同源策略會導致跨域,這里同源策略又分為以下兩種:

  DOM同源策略:禁止對不同源頁面DOM進行操作。這里主要場景是iframe跨域的情況,不同域名的iframe是限制互相訪問的。

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

二、為什么要有跨域限制

  了解完跨域之后,想必大家都會有這么一個思考,為什么要有跨域的限制,瀏覽器這么做是出於何種原因呢。其實仔細想一想就會明白,跨域限制主要是為了安全考慮。

  常見的跨域場景:

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,那么可以做如下攻擊:

  1、用戶登錄了自己的銀行頁面http://mybank.com,http://mybank.com向用戶的cookie中添加用戶標識

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

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

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

  5、而且由於Ajax在后台執行,用戶無法感知這一過程。

(2)DOM同源策略也一樣,如果iframe之間可以跨域訪問,可以這樣攻擊:

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

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

  3、這時如果用戶輸入賬號密碼,我們的主網站可以跨域訪問到http://mybank.com的dom節點,就可以拿到用戶的輸入了,那么就完成了一次攻擊。 所以說有了跨域跨域限制之后,我們才能更安全的上網了。

三、跨域的解決方式

  (1)跨域資源共享

  CORS是一個W3C標准,全稱是”跨域資源共享”(Cross-origin resource sharing)。 對於這個方式,阮一峰老師總結的文章特別好,希望深入了解的可以看一下http://www.ruanyifeng.com/blog/2016/04/cors.html。

  這里簡單的說一說大體流程。

  1、對於客戶端,我們還是正常使用xhr對象發送ajax請求。 唯一需要注意的是,我們需要設置我們的xhr屬性withCredentials為true,不然的話,cookie是帶不過去的,設置: xhr.withCredentials = true;

  2、對於服務器端,需要在 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代理跨域

  1、nginx配置解決iconfont跨域

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

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

  2、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寫入,方便接口登錄認證。

  1、 非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中

  2、 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等常見方法就可以了。不過遇到了一些非常規情況,我們還是需要知道有更多的方法可以選擇的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM