瀏覽器的跨域問題以及解決方案

一、爲何會有跨域問題的存在?javascript

   JavaScript出於安全方面的考慮,不容許跨域調用其餘頁面的對象,即同源政策。php

 

二、什麼是同源?html

   1995年,同源政策由 Netscape 公司引入瀏覽器。目前,全部瀏覽器都實行這個政策。
  最初,它的含義是指,A網頁設置的 Cookie,B網頁不能打開,除非這兩個網頁"同源"。所謂"同源"指的是"三個相同"。前端

  (1)協議相同html5

  (2)域名相同java

  (3)端口相同web

具體實例  好比:http://www.example.com/zw/index.html這個網站,它的協議是http://,域名是www.example.com,端口號是80(默認端口能夠省略),它的同源狀況以下:express

   ①、http://www.example.com/zwxk/manager.html    同源json

   ②、https://www.example.com/zw/index.html   不一樣源(協議不一樣)跨域

   ③、http://examle.com/zw/index.html  不一樣源(域名不一樣)

   ④、http://www.example.com:81zw/index.html   不一樣源(端口號不一樣)

 同源政策的目的

同源策略的目的是爲了保證用戶信息的安全。防止惡意的網站盜取數據。

設想這樣一個情景:A網站是一家銀行,用戶登陸之後,又去瀏覽其餘的網站B,若是網站B能夠讀取A網站的Cookie,會發生什麼問題?

顯然,若是Cookie包含隱私(好比存款總額),這些信息就會泄露,更可怕的是,Cookie每每用來保存用戶的登陸狀態,若是用戶沒有退出登陸,其餘網站就能夠冒用戶,隨心所欲。由於瀏覽器同時還規定,提交表單不受同源策略的限制。

因而可知,「同源政策」的必要性,不然Cookie能夠共享,互聯網就毫無安全可言了。

 

三、非同源限制範圍

     隨着互聯網的發展,「同源政策」愈來愈嚴格。目前,若是非同源,共有三種行爲受到限制。

  (1)Cookie、LocalStorage和IndexDB沒法獲取。

  (2)DOM沒法得到。

  (3)AJAX請求不能發送。

雖然這些限制是必要的,可是有時很不方便,合理的用途也受到影響。下面將介紹如何規避上面三種限制。

 

4-一、解決跨域一:Cookie如何實現跨域

     Cookie是服務器寫入瀏覽器的一段信息,只有同源的網頁才能共享,可是,兩個網頁一級域名相同,只是二級域名不一樣,瀏覽器容許經過設置document.domain共Cookie。

舉例來講,A網站是:http:weibo.qq.com

  B網站是:http:lol.qq.com

 

那麼只需設置相同的document.domain,兩個網頁就可共享Cookie。

 

[javascript]  view plain  copy
 
  1. document.domain = 'qq.com';  

 

如今,A網頁經過腳本設置一個Cookie。

 

[javascript]  view plain  copy
 
  1. document.Cookie = "test1=hello";  

 

B網頁就能讀到這個Cookie。

 

[javascript]  view plain  copy
 
  1. var getCookie = document.cookie;  

 

注意:這種方法只適用於Cookie和iframe窗口,LocalStorage和IndexDB沒法經過這種方法規避,而要使用下文將介紹的PostMessage API。

另外,服務器也能夠在設置Cookie的時候,指定Cookie的所屬域名爲一級域名,好比:.qq.com

 

[javascript]  view plain  copy
 
  1. Set-Cookie:key=value;domain=<span style="color:#cc0000;">.qq.com</span>;path=/  

 

 

這樣的話,二級域名和三級域名不用作任何設置,均可以讀取這個Cookie。

 

4-二、解決跨域問題二:如何跨域獲取DOM。

若是兩個網頁不一樣源,就沒法拿到對方的DOM。典型的例子是iframe窗口和window.open方法打開的窗口,它們與父窗口沒法通訊。

   好比,父窗口運行下面的命令,若是iframe窗口不是同源將會報錯。

 

[javascript]  view plain  copy
 
  1. document.getElementById("iframe").contentWindow.document  

 

   上面命令中,父窗口想獲取子窗口的DOM,應爲跨源致使報錯。

   反之亦然,子窗口獲取主窗口的DOM也會報錯。

 

[javascript]  view plain  copy
 
  1. window.parent.document.body  

 

   若是兩個窗口一級域名相同,只是二級域名不一樣,那麼設置4-1介紹的document.domain屬性,就可規避同源政策,拿到DOM。

   對於徹底不相同的網站,目前有三種方法,能夠解決跨域窗口的通訊問題。

   (1)片斷識別符(fragment identifier)

   (2)window.name

   (3)跨文檔通訊API(Cross-document messaging)

 

 4-2-1:片斷識別符

     片斷識別符指的是,URL的#號後面的部分,好比http://qq.com/x.html#fragment的#fragment。若是隻是改變片斷標識符,頁面將不會從新刷新、

  父窗口能夠把信息,寫入子窗口的片斷標識符。

 

[javascript]  view plain  copy
 
  1. var src = originURL+'#'+data;  
  2. document.getElementById('iframe').src = src;  

 

  子窗口經過監聽hashchange事件獲得通知

 

[javascript]  view plain  copy
 
  1.   window.onhashchange = checkMessage;  
  2.   function checkMessage(){  
  3.   var message = window.location.hash;  
  4.   //...  
  5.  }  

 

 

 一樣的,子窗口也能夠改變父窗口的片斷標識符。

 

[javascript]  view plain  copy
 
  1. parent.location.href = target+"#"+hash;  

 

4-2-2:window.name:

瀏覽器窗口有window.name屬性。這個屬性的最大特色是,不管是否同源,只要在同一個窗口裏,前一個網頁設置這個屬性,後一個網頁就能夠讀取它。

父窗口先發開一個子窗口,載入一個不一樣源的網頁,該網頁將信息寫入window.name屬性。

 

[javascript]  view plain  copy
 
  1. window.name = data;  
 

 

接着,子窗口跳回一個與主窗口同域的網址。

 

[javascript]  view plain  copy
 
  1. location = 'http://parent.url.com/xxx.html';  

  而後,主窗口就能夠讀取子窗口的window.name了。

 

[javascript]  view plain  copy
 
  1. var data = document.getElementById('iframe').contentWindow.name;  

 

優勢:window.name容量很大,能夠防止很是長的字符串;

 

缺點:必須監聽子窗口window.name屬性的變化,會影響網頁性能。

 

4-2-3:跨文檔消息傳輸window.postMessage:

上面兩種方法都屬於破解,HTML5爲解決這個問題,引入一個全新的API:跨文檔消息傳輸Cross Document Messaging。

下一代瀏覽器都將支持這個功能:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 。 

Facebook已經使用了這個功能,用postMessage支持基於web的實時消息傳遞。


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

otherWindow: 對接收信息頁面的window的引用。能夠是頁面中iframe的contentWindow屬性;window.open的返回值;

經過name或下標從window.frames取到的值。

message: 具體的信息內容,string類型。

targetOrigin: 接受消息的窗口的源(origin),即「協議+域名+端口」。也能夠設爲「*」,表示不限制域名,向全部窗口發送。

message事件的事件對象event,提供一下三個屬性:

(1).event.source:發送消息的窗口

(2).event.origin:消息發向的網站

(3).event.data:消息內容

具體實例:

a.com/index.html中的代碼:

 

[javascript]  view plain  copy
 
  1. <iframe id="ifr" src="b.com/index.html"></iframe>  
  2. <script type="text/javascript">  
  3. window.onload = function() {  
  4.     var ifr = document.getElementById('ifr');  
  5.     var targetOrigin = 'http://b.com';  // 若寫成'http://b.com/c/proxy.html'效果同樣  
  6.                                         // 若寫成'http://c.com'就不會執行postMessage了  
  7.     ifr.contentWindow.postMessage('I was there!', targetOrigin);  
  8. };  
  9. </script>  

 

b.com/index.html中的代碼:

 

[javascript]  view plain  copy
 
  1. <script type="text/javascript">  
  2.     window.addEventListener('message', function(event){  
  3.         // 經過origin屬性判斷消息來源地址  
  4.         if (event.origin == 'http://a.com') {  
  5.             alert(event.data);    // 彈出"I was there!"  
  6.             alert(event.source);  // 對a.com、index.html中window對象的引用  
  7.                                   // 但因爲同源策略,這裏event.source不能夠訪問window對象  
  8.         }  
  9.     }, false);  
  10. </script>  

 

4-三、如何解決跨域LocalStorage。

經過window.postMessage,讀寫其餘窗口的LocalStorage也成爲了可能。下面是一個例子,主窗口寫入iframe子窗口的LocalStorage。

 

[javascript]  view plain  copy
 
  1. window.onmessage = function(e){  
  2.   if(e.origin !== 'http://bbb.com'){  
  3.     return ;  
  4.  }  
  5.   var payload = JSON.parse(e.data);  
  6.   localStorage.setItem(payload.key,JSON.stringify(payload.data));  
  7. };  

 

 

上面代碼中,子窗口將父窗口發送來的消息,寫入本身的LocalStorage。

父窗口發送消息的代碼以下.

 

[javascript]  view plain  copy
 
  1. var win = document.getElementsByTagName('iframe')[0].contentWindow;  
  2. var obj = {name:'Jack'};  
  3. win.postMessage(JSON.stringify({key:'storage',data:obj}),'http://bbb.com');  

 

增強版的子窗口接受消息的代碼以下。

[javascript]  view plain  copy
 
  1. window.onmessage = function(e) {  
  2.   if (e.origin !== 'http://bbb.com') return;  
  3.   var payload = JSON.parse(e.data);  
  4.   switch (payload.method) {  
  5.     case 'set':  
  6.       localStorage.setItem(payload.key, JSON.stringify(payload.data));  
  7.       break;  
  8.     case 'get':  
  9.       var parent = window.parent;  
  10.       var data = localStorage.getItem(payload.key);  
  11.       parent.postMessage(data, 'http://aaa.com');  
  12.       break;  
  13.     case 'remove':  
  14.       localStorage.removeItem(payload.key);  
  15.       break;  
  16.   }  
  17. };  
 

增強版的父窗口發送消息代碼以下:

 

[javascript]  view plain  copy
 
  1. var win = document.getElementsByTagName('iframe')[0].contentWindow;  
  2. var obj = { name: 'Jack' };  
  3. // 存入對象  
  4. win.postMessage(JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://bbb.com');  
  5. // 讀取對象  
  6. win.postMessage(JSON.stringify({key: 'storage', method: "get"}), "*");  
  7. window.onmessage = function(e) {  
  8.   if (e.origin != 'http://aaa.com') return;  
  9.   // "Jack"  
  10.   console.log(JSON.parse(e.data).name);  
  11. };  

 

 

4-四、如何解決AJAX的跨域:

同源政策規定,AJAX請求只能發給同源的網址,不然就報錯。
除了架設服務器代理(瀏覽器請求同源服務器,再由後者請求外部服務),有三種方法規避這個限制。

(1)JSONP

(2)WebSocket

(3)CORS

 

4-4-一、JSONP解決AJAX跨域問題:

JSONP是服務器與客戶端跨源通訊的經常使用方法。最大特色就是簡單適用,老式瀏覽器所有支持,服務器改造很是小。
它的基本思想是,網頁經過添加一個<script>元素,向服務器請求JSON數據,這種作法不受同源政策限制;服務器收到請求後,將數據放在一個指定名字的回調函數裏傳回來。
首先,網頁動態插入<script>元素,由它向跨源網址發出請求。

 

[javascript]  view plain  copy
 
  1. function addScriptTag(src) {  
  2.   var script = document.createElement('script');  
  3.   script.setAttribute("type","text/javascript");  
  4.   script.src = src;  
  5.   document.body.appendChild(script);  
  6. }  
  7.   
  8. window.onload = function () {  
  9.   addScriptTag('http://example.com/ip?callback=foo');  
  10. }  
  11.   
  12. function foo(data) {  
  13.   console.log('Your public IP address is: ' + data.ip);  
  14. };  

上面代碼經過動態添加<script>元素,向服務器example.com發出請求。注意,該請求的查詢字符串有一個callback參數,用來指定回調函數的名字,這對於JSONP是必需的。
服務器收到這個請求之後,會將數據放在回調函數的參數位置返回。

 

[javascript]  view plain  copy
 
  1. foo({  
  2.   "ip": "8.8.8.8"  
  3. });  

因爲<script>元素請求的腳本,直接做爲代碼運行。這時,只要瀏覽器定義了foo函數,該函數就會當即調用。做爲參數的JSON數據被視爲javascript對象,而不是字符串,所以避免了使用JSON.parse的步驟。

 

4-4-二、經過webSocket解決AJAX跨域

WebSocket是一種通訊協議,使用ws://(非加密)和wss://(加密)做爲協議前綴。該協議不實行同源政策,只要服務器支持,就能夠經過它進行跨源通訊。

   下面是一個例子,瀏覽器發出的WebSocket請求的頭信息(摘自維基百科)。

 

[javascript]  view plain  copy
 
  1. GET /chat HTTP/1.1  
  2. Host: server.example.com  
  3. Upgrade: websocket  
  4. Connection: Upgrade  
  5. Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==  
  6. Sec-WebSocket-Protocol: chat, superchat  
  7. Sec-WebSocket-Version: 13  
  8. Origin: http://example.com  

 

上面代碼中,有一個字段是Origin,表示該請求的請求源(origin),即發自哪一個域名。
正是由於有了Origin這個字段,因此WebSocket纔沒有實行同源政策。由於服務器能夠根據這個字段,判斷是否許可本次通訊。若是該域名在白名單內,服務器就會作出以下回應。

 

[javascript]  view plain  copy
 
  1. HTTP/1.1 101 Switching Protocols  
  2. Upgrade: websocket  
  3. Connection: Upgrade  
  4. Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=  
  5. Sec-WebSocket-Protocol: chat  

 

 

4-4-三、經過CORS解決AJAX跨域

CORS是跨源資源分享(Cross-Origin Resource Sharing)的縮寫。它是W3C標準,是跨源AJAX請求的根本解決方法。相比JSONP只能發GET請求,CORS容許任何類型的請求。

定義:CORS其實出現時間不短了,它在維基百科上的定義是:跨域資源共享(CORS)是一種網絡瀏覽器的技術規範,它爲Web服務器定義了一種方式,容許網頁從不一樣的域訪問其資源。而這種訪問是被同源策略所禁止的。CORS系統定義了一種瀏覽器和服務器交互的方式來肯定是否容許跨域請求。 它是一個妥協,有更大的靈活性,但比起簡單地容許全部這些的要求來講更加安全。而W3C的官方文檔目前仍是工做草案,可是正在朝着W3C推薦的方向前進。

   簡言之,CORS就是爲了讓AJAX能夠實現可控的跨域訪問而生的。

 

以往的解決方案:

  之前要實現跨域訪問,能夠經過JSONP、Flash或者服務器中轉的方式來實現,可是如今咱們有了CORS。

  CORS與JSONP相比,無疑更爲先進、方便和可靠。

    一、 JSONP只能實現GET請求,而CORS支持全部類型的HTTP請求。

    二、 使用CORS,開發者可使用普通的XMLHttpRequest發起請求和得到數據,比起JSONP有更好的錯誤處理。

    三、 JSONP主要被老的瀏覽器支持,它們每每不支持CORS,而絕大多數現代瀏覽器都已經支持了CORS(這部分會在後文瀏覽器支持部分介紹)。

詳細內容:

要使用CORS,咱們須要瞭解前端和服務器端的使用方法。

一、前端

   之前咱們使用Ajax,代碼相似於以下的方式:

 

[javascript]  view plain  copy
 
  1. var xhr = new XMLHttpRequest();    
  2. xhr.open("GET", "/relativeHref", true);    
  3. xhr.send();  

 

    這裏的「/relativeHref」是本域的相對路徑。

 

    若是咱們要使用CORS,相關Ajax代碼可能以下所示:

 

[javascript]  view plain  copy
 
  1. var xhr = new XMLHttpRequest();    
  2. xhr.open("GET", "http://blog.csdn.net/hfahe", true);    
  3. xhr.send();    

 

 

    請注意,代碼與以前的區別就在於相對路徑換成了其餘域的絕對路徑,也就是你要跨域訪問的接口地址。

    咱們還必須提供瀏覽器回退功能檢測和支持,避免瀏覽器不支持的狀況。

 

[javascript]  view plain  copy
 
  1. function createCORSRequest(method, url) {    
  2.   var xhr = new XMLHttpRequest();    
  3.   if ("withCredentials" in xhr) {    
  4.     // 此時即支持CORS的狀況    
  5.     // 檢查XMLHttpRequest對象是否有「withCredentials」屬性    
  6.     // 「withCredentials」僅存在於XMLHTTPRequest2對象裏    
  7.     xhr.open(method, url, true);    
  8.   } else if (typeof!= "undefined") {    
  9.     // 不然檢查是否支持XDomainRequest,IE8和IE9支持    
  10.     // XDomainRequest僅存在於IE中,是IE用於支持CORS請求的方式    
  11.     xhr = new XDomainRequest();    
  12.     xhr.open(method, url);    
  13.   } else {  
  14.     // 不然,瀏覽器不支持CORS    
  15.     xhr = null;    
  16.   }    
  17.   return xhr;    
  18. }    
  19.      
  20. var xhr = createCORSRequest('GET', url);    
  21. if (!xhr) {    
  22.   throw new Error('CORS not supported');    
  23. }    

 

 

如今若是直接使用上面的腳本進行請求,會看到瀏覽器裏控制檯的報錯以下:

錯誤顯示的很明顯,這是由於咱們還未設置Access-Control-Allow-Origin頭。

 

二、服務器

服務器端對於CORS的支持,主要就是經過設置Access-Control-Allow-Origin來進行的。若是瀏覽器檢測到相應的設置,就能夠容許Ajax進行跨域的訪問。

HTTP 頭的設置方法有不少,http://enable-cors.org/這篇文章裏對各類服務器和語言的設置都有詳細的介紹:

①、Apache:Apache須要使用mod_headers模塊來激活HTTP頭的設置,它默認是激活的。你只須要在Apache配置文件的<Directory>, <Location>, <Files>或<VirtualHost>的配置里加入如下內容便可:

 

[javascript]  view plain  copy
 
  1. Header set Access-Control-Allow-Origin *    

 

     ②、PHP:只須要使用以下的代碼設置便可。

 

[javascript]  view plain  copy
 
  1. <?php    
  2.      header("Access-Control-Allow-Origin:*");    

 

     以上的配置的含義是容許任何域發起的請求均可以獲取當前服務器的數據。固然,這樣有很大的危險性,惡意站點可能經過XSS攻擊咱們的服務器。因此咱們應該儘可能有針對性的對限制安全的來源,例以下面的設置使得只有http://blog.csdn.NET這個域才能跨域訪問服務器的API。

 

[javascript]  view plain  copy
 
  1. Access-Control-Allow-Origin: http://blog.csdn.net    

 

③、Node的配置(基於express):

 

[javascript]  view plain  copy
 
  1. var express = require('express');    
  2. var app = express();    
  3. //設置跨域訪問    
  4. app.all('*', function(req, res, next) {    
  5.     res.header("Access-Control-Allow-Origin", "*");    
  6.     res.header("Access-Control-Allow-Headers", "X-Requested-With");    
  7.     res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");    
  8.     res.header("X-Powered-By",' 3.2.1')    
  9.     res.header("Content-Type", "application/json;charset=utf-8");    
  10.     next();    
  11. });    
  12. app.get('/auth/:id/:password', function(req, res) {    
  13.     res.send({id:req.params.id, name: req.params.password});    
  14. });    
  15. app.listen(3000);   

 

 

瀏覽器支持狀況

上圖爲各瀏覽器對於CORS的支持狀況(綠色爲支持,數據來源:http://caniuse.com/cors),看起來至關樂觀。主流瀏覽器都已基本提供對跨域資源共享的支持,因此,CORS纔會在國外使用的如此廣泛。

 

將來

從全部的瀏覽器都支持來看,CORS將成爲將來跨域訪問的標準解決方案。不管是本身服務器間的跨域訪問,仍是開放平臺爲第三方提供API,都將採用這種統一的解決方案,由於它簡單、高效,受到全部主流瀏覽器的支持。它很是重要,也會讓咱們的網絡變得更加開放。

 

本文部份內容轉自http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html

相關文章
相關標籤/搜索