JavaScript跨域
js跨域是指通過js在不同的域之間進行數據傳輸或通信,比如用ajax向一個不同的域請求數據,或者通過js獲取頁面中不同域的框架中(iframe)的數據。只要協議、域名、端口有任何一個不同,都被當作是不同的域。
同源策略阻止從一個域上加載的腳本獲取或操作另一個域上的文檔屬性。也就是說,受到請求的 URL 的域必須與當前 Web 頁面的域相同。這意味着瀏覽器隔離來自不同源的內容,以防止它們之間的操作。
URL | 說明 | 是否允許通信 |
http://www.a.com/a.js |
同一域名下 | 允許 |
|
|
允許 |
|
同一域名,不同端口 | 不允許 |
|
|
不允許 |
|
域名和域名對應ip | 不允許 |
http://www.a.com/a.js |
主域相同,子域不同 | 不允許 |
http://www.a.com/a.js |
同一域名,不同二級域名(同上) | 不允許(cookie這種情況下也不允許訪問) |
http://www.cnblogs.com/a.js |
不同域名 | 不允許 |
JavaScript出於安全方面的考慮,同源策略不允許跨域調用其他頁面的對象。但在安全限制的同時也給注入iframe或是ajax應用上帶來了不少麻煩。
javascript跨域簡單分為以下情況:
1、基於同一父域的子域之間,如:a.c.com和b.c.com
2、基於不同的父域之間,如:www.a.com和www.b.com
3、端口的不同,如:www.a.com:8080和www.a.com:8088
4、協議不同,如:http://www.a.com和https://www.a.com
對於端口和協議的不同,需要通過后台proxy來解決,具體方式如下:
a、在發起方的域下創建proxy程序
b、發起方的js調用本域下的proxy程序
c、proxy將請求發送給接收方並獲取相應數據
d、proxy將獲得的數據返回給發起方的js
而情況1和2除了通過后台proxy這種方式外,還可以有幾種辦法來解決:
一.JSONP
JSON is a subset of the object literal notation of JavaScript. Since JSON is a subset of JavaScript, it can be used in the language with no muss or fuss.
1.什么是JSONP
JSONP(JSON with Padding)是一個非官方的協議,它允許在服務器端集成Script tags返回至客戶端,通過javascript callback的形式實現跨域訪問(這僅僅是JSONP簡單的實現形式)。單向的數據請求。
2.JSONP的原理與實現思路
1)Web頁面調用js文件,可跨域。擴展:但凡有src屬性的標簽都具有跨域能力。
2)跨域服務器 動態生成數據 並存入js文件(通常json后綴),供客戶端調用。
3)為了便於客戶端使用數據,形成一個非正式傳輸協議,稱為JSONP。該協議重點是允許用戶傳遞一個callback參數給服務器,然后服務器返回數據時 將此callback參數作為函數名包裹住JSON數據,使得客戶端可以隨意定制自己的函數來自動處理返回數據。
3.使用JSONP
動態創建script,添加到head中(你可以往body中添加)。
在發起方頁面動態加載一個script,script的URL指向接收方的一個處理地址(后台),該地址返回的javascript方法會被執行,另外URL中可以傳入一些參數,該方法只支持GET方式提交參數。
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" /> <script type="text/javascript"> function jsonpCallback(result) { //alert(result); for(var i in result) { alert(i+":"+result[i]);//循環輸出a:1,b:2,etc. } } var JSONP=document.createElement("script"); JSONP.type="text/javascript"; JSONP.src="http://crossdomain.com/services.php?callback=jsonpCallback"; document.getElementsByTagName("head")[0].appendChild(JSONP); </script>
js文件載入成功后會執行我們在url參數中指定的函數,並且會把我們需要的json數據作為參數傳入。所以jsonp是需要服務器端的頁面進行相應的配合的。
接收方服務器端代碼(php)如下:
<?php $callback = $_GET['callback'];//得到回調函數名 $data = array('a','b','c');//要返回的數據 echo $callback.'('.json_encode($data).')';//輸出 ?>
JSONP易於實現,但是也會存在一些安全隱患,如果第三方的腳本隨意地執行,那么它就可以篡改頁面內容,截獲敏感數據。但是在受信任的雙方傳遞數據,JSONP是非常合適的選擇。
二.通過修改document.domain來跨子域
瀏覽器都有一個同源策略,其限制之一就是第一種方法中我們說的不能通過ajax的方法去請求不同源中的文檔。 它的第二個限制是瀏覽器中不同域的框架之間是不能進行js的交互操作的。主域相同,子域不同。
需要說明的是,不同的框架之間(父子或同輩),是能夠獲取到彼此的window對象的,但獲取到的window對象幾乎無用(html5中的postMessage方法是一個例外)。
1.document.domain用於不同子域的框架間的交互
如,有一個頁面,它的地址是http://www.example.com/a.html , 在這個頁面里面有一個iframe,它的src是http://example.com/b.html, 很顯然,這個頁面與它里面的iframe框架是不同域的,所以我們是無法通過在頁面中書寫js代碼來獲取iframe中的東西的。
我們只要把http://www.example.com/a.html 和 http://example.com/b.html這兩個頁面的document.domain都設成相同的域名就可以了。
但要注意的是,document.domain的設置是有限制的,我們只能把document.domain設置成自身或更高一級的父域,且主域必須相同。例如:a.b.example.com 中某個文檔的document.domain 可以設成a.b.example.com、b.example.com 、example.com中的任意一個,但是不可以設成 c.a.b.example.com,因為這是當前域的子域,也不可以設成baidu.com,因為主域已經不相同了。
在頁面 http://www.example.com/a.html 中設置document.domain:
<iframe src="http://example.com/b.html" id="iframe" onload="test()"></iframe> <script> document.domain = 'example.com';//設置成主域 function test(){ alert(document.getElementById('iframe').contentWindow); } </script>
在頁面 http://example.com/b.html 中也設置document.domain,而且這也是必須的,雖然這個文檔的domain就是example.com,但是還是必須顯示的設置
<script> document.domain = 'example.com';//設置成主域 </script>
這樣我們就可以通過js訪問到iframe中的各種屬性和對象了。
不過如果你想在http://www.example.com/a.html 頁面中通過ajax直接請求http://example.com/b.html 頁面,即使你設置了相同的document.domain也還是不行的,所以修改document.domain的方法只適用於不同子域的框架間的交互。
如果你想通過ajax的方法去與不同子域的頁面交互,除了使用jsonp的方法外,還可以用一個隱藏的iframe來做一個代理。原理就是讓這個iframe載入一個與你想要通過ajax獲取數據的目標頁面處在相同的域的頁面,所以這個iframe中的頁面是可以正常使用ajax去獲取你要的數據的,然后就是通過我們剛剛講得修改document.domain的方法,讓我們能通過js完全控制這個iframe,這樣我們就可以讓iframe去發送ajax請求,然后收到的數據我們也可以獲得了。
2.document.domain和iframe實現站內AJAX跨域
如:A:http://www.css88.com/demo/iframe-domain/要獲取B:http://css88.com/demo/iframe-domain/city.html的數據,在A 頁面內動態插入一個與B相同域的iframe C:http://css88.com/demo/domain/iframe.html 。B與C可以通信,A通過控制C來獲取B的數據。代碼如下:
document.domain = "css88.com"; var createAjaxIframe={ appIframe: function(iframeId, iframeSrc){ var iframe = document.createElement("iframe"); iframe.src = iframeSrc// "http://css88.com/demo/domain/iframe.html"; iframe.id = iframeId; iframe.style.display = "none"; if (iframe.attachEvent) { iframe.attachEvent("onload", function(){ createAjaxIframe.domainAjax(iframeId); }); }else { iframe.onload = function(){ createAjaxIframe.domainAjax(iframeId); }; } document.body.appendChild(iframe); }, domainAjax: function(iframeId){ var iframeDom = document.getElementById(iframeId).contentWindow.$; iframeDom.getJSON("http://css88.com/demo/iframe-domain/city.html", function(date){ var cityOption = ""; for (i = 0; i < date.length; i++) { cityOption += date[i].c_name + "--" + date[i].c_value + "<br />" } $("#test").html(cityOption); }); } }; createAjaxIframe.appIframe("iframe",http://css88.com/demo/iframe-domain/iframe.html);
三.window.name實現的跨域數據傳輸
window對象有個name屬性,該屬性有個特征:即在一個窗口(window)的生命周期內,窗口載入的所有的頁面都是共享一個window.name的,每個頁面對window.name都有讀寫的權限,window.name是持久存在一個窗口載入過的所有頁面中的,並不會因新頁面的載入而進行重置。
原理
有三個頁面:
a.com/app.html:應用頁面。
a.com/proxy.html:代理文件,一般是一個沒有任何內容的html文件,需要和應用頁面在同一域下。在應用頁面動態創建iframe
b.com/data.html:應用頁面需要獲取數據的頁面,可稱為數據頁面。
總結起來即:iframe的src屬性由外域轉向本地域,跨域數據即由iframe的window.name從外域傳遞到本地域。這個就巧妙地繞過了瀏覽器的跨域訪問限制,但同時它又是安全操作。
實現起來基本步驟如下:
1.數據頁面b.com/data.html代碼如下:
<script type="text/javascript"> window.name = 'I was there!'; // 這里是要傳輸的數據,大小一般為2M,IE和firefox下可以 大至32M左右 // 數據格式可以自定義,如json、字符串 </script>
2.應用頁面a.com/app.html代碼如下:
<!DOCTYPE> <html> <head> <title>跨域獲取數據</title> <script type="text/javascript"> function domainData(url, fn) { var isFirst = true; var iframe = document.createElement('iframe'); iframe.style.display = 'none'; var loadfn = function(){ if(isFirst){ iframe.contentWindow.location = 'http://a.com/proxy.html';// 設置的代理文件 isFirst = false; } else { fn(iframe.contentWindow.name); //獲取數據以后銷毀這個iframe,釋放內存;這也保證了安全(不被其他域frame js訪問) iframe.contentWindow.document.write(''); iframe.contentWindow.close(); document.body.removeChild(iframe); iframe.src = ''; iframe = null; } }; iframe.src = url; if(iframe.attachEvent){ iframe.attachEvent('onload', loadfn); } else { iframe.onload = loadfn; } document.body.appendChild(iframe); } </script> </head> <body> </body> <script type="text/javascript"> domainData('b.com/data.html', function(data){ alert(data); }); </script> </html>
四.利用location.hash+iframe跨域獲取數據
如果看懂了window.name+iframe跨域獲取數據 ,那么這個就很好理解。一樣都是動態插入一個iframe,然后把iframe的src指向服務端地址,而服務端同樣都是輸出一段js代碼,同樣都是利用和子窗口之間的通信完成數據傳輸,同樣要針對同源策略做出處理。
location是javascript里邊管理地址欄的內置對象,比如location.href就管理頁面的url,用location.href=url就可以直接將頁面重定向url。而location.hash則可以用來獲取或設置頁面的標簽值。比如http://domain/#admin的location.hash="#admin"。
其實很簡單,如果index頁面要獲取遠端服務器的數據,動態插入一個iframe,將iframe的src屬性指向服務端地址。這時top window和包裹這個iframe的子窗口是不能通信的(同源策略),所以改變子窗口的路徑就行了,將數據當做改變后的路徑的hash值加在路徑上,然后就能通信了(和window.name跨域幾乎相同),將數據加在index頁面地址的hash值上。index頁面監聽地址的hash值變化(html5有hashchange事件,用setInterval不斷輪詢判斷兼容ie6/7),然后做出判斷,處理數據。
<body> <script type="text/javascript"> function getData(url, fn) { var iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.src = url; iframe.onload = function() { fn(iframe.contentWindow.location.hash.substring(1)); window.location.hash = ''; document.body.removeChild(iframe); }; document.body.appendChild(iframe); } // get data from server var url = 'http://localhost:8080/data.php'; getData(url, function(data) { var jsondata = JSON.parse(data); console.log(jsondata.name + ' ' + jsondata.age); }); </script> </body>
<?php // 如果有必要則進行數據處理 $_GET['..'] // code // 返回的數據 $data = '{\"name\":\"hanzichi\",\"age\":10}'; echo " <script> window.location = 'http://localhost:81/location-hash/proxy.html' + '#' + \"$data\"; </script> " ?>
location.hash+iframe法和jsonp以及window.name+iframe一樣,都是雙向的,但是都只能是GET形式,所以數據只能加在url上。
五.使用HTML5中新引進的window.postMessage方法來跨域傳送數據
postMessage()方法允許來自不同源的腳本采用異步方式進行有限的通信,可以實現跨文本檔、多窗口、跨域消息傳遞。
window.postMessage(message,targetOrigin) 方法是html5新引進的特性,可以使用它來向其它的window對象發送消息,無論這個window對象是屬於同源或不同源,目前IE8+、FireFox、Chrome、Opera等瀏覽器都已經支持window.postMessage方法。
postMessage(data,origin)方法接受兩個參數
1.data:要傳遞的數據,html5規范中提到該參數可以是JavaScript的任意基本類型或可復制的對象,然而並不是所有瀏覽器都做到了這點兒,部分瀏覽器只能處理字符串參數,所以我們在傳遞參數的時候需要使用JSON.stringify()方法對對象參數序列化,在低版本IE中引用json2.js可以實現類似效果。
2.origin:字符串參數,指明目標窗口的源,協議+主機+端口號[+URL],URL會被忽略,所以可以不寫,這個參數是為了安全考慮,postMessage()方法只會將message傳遞給指定窗口,當然如果願意也可以建參數設置為"*",這樣可以傳遞給任意窗口,如果要指定和當前窗口同源的話設置為"/"。
左邊的div會根據右邊iframe內div顏色變化而變化
1.主頁:http://test.com/index.html
<!DOCTYPE html> 2 <html> 3 <head> 4 <title>Post Message</title> 5 </head> 6 <body> 7 <div style="width:200px; float:left; margin-right:200px;border:solid 1px #333;"> 8 <div id="color">Frame Color</div> 9 </div> 10 <div> 11 <iframe id="child" src="http://lsLib.com/lsLib.html"></iframe> 12 </div> 13 14 <script type="text/javascript"> 15 16 window.onload=function(){ 17 window.frames[0].postMessage('getcolor','http://lslib.com'); 18 } 19 20 window.addEventListener('message',function(e){ 21 var color=e.data; 22 document.getElementById('color').style.backgroundColor=color; 23 },false); 24 </script> 25 </body> 26 </html>
2.主頁內iframe: http://lslib.com/lslib.html
<!doctype html> 2 <html> 3 <head> 4 <style type="text/css"> 5 html,body{ 6 height:100%; 7 margin:0px; 8 } 9 </style> 10 </head> 11 <body style="height:100%;"> 12 <div id="container" onclick="changeColor();" style="widht:100%; height:100%; background-color:rgb(204, 102, 0);"> 13 click to change color 14 </div> 15 <script type="text/javascript"> 16 var container=document.getElementById('container'); 17 18 window.addEventListener('message',function(e){ 19 if(e.source!=window.parent) return; 20 var color=container.style.backgroundColor; 21 window.parent.postMessage(color,'*'); 22 },false); 23 24 function changeColor () { 25 var color=container.style.backgroundColor; 26 if(color=='rgb(204, 102, 0)'){ 27 color='rgb(204, 204, 0)'; 28 }else{ 29 color='rgb(204,102,0)'; 30 } 31 container.style.backgroundColor=color; 32 window.parent.postMessage(color,'*'); 33 } 34 </script> 35 </body> 36 </html>
在例子中頁面加載的時候主頁面向iframe發送’getColor‘ 請求(參數沒實際用處)
window.onload=function(){ window.frames[0].postMessage('getcolor','http://lslib.com'); }
iframe接收消息,並把當前顏色發送給主頁面
window.addEventListener('message',function(e){ if(e.source!=window.parent) return; var color=container.style.backgroundColor; window.parent.postMessage(color,'*'); },false);
主頁面接收消息,更改自己div顏色
window.addEventListener('message',function(e){ var color=e.data; document.getElementById('color').style.backgroundColor=color; },false);
當點擊iframe事觸發其變色方法,把最新顏色發送給主頁面
function changeColor () { var color=container.style.backgroundColor; if(color=='rgb(204, 102, 0)'){ color='rgb(204, 204, 0)'; }else{ color='rgb(204,102,0)'; } container.style.backgroundColor=color; window.parent.postMessage(color,'*'); }
主頁面還是利用剛才監聽message事件的程序處理自身變色
window.addEventListener('message',function(e){ var color=e.data; document.getElementById('color').style.backgroundColor=color; },false);
使用postMessage來跨域傳送數據還是比較直觀和方便的,但是缺點是IE6、IE7不支持。
六.CORS: 跨域資源共享(Cross-Origin Resource Sharing)
當前幾乎所有的瀏覽器(Internet Explorer 8+, Firefox 3.5+, Safari 4+和 Chrome 3+)都可通過名為跨域資源共享(Cross-Origin Resource Sharing)的協議支持ajax跨域調用。
啟用 CORS 請求
假設您的應用已經在 example.com 上了,而您想要從 www.example2.com 提取數據。一般情況下,如果您嘗試進行這種類型的 AJAX 調用,請求將會失敗,而瀏覽器將會出現“源不匹配”的錯誤。利用 CORS,www.example2.com 服務端只需添加一個HTTP Response頭,就可以允許來自 example.com 的請求:
Access-Control-Allow-Origin: http://example.com Access-Control-Allow-Credentials: true(可選)
Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true(可選)
提交跨域請求
如果服務器端已啟用了 CORS,那么提交跨域請求就和普通的 XMLHttpRequest 請求沒什么區別。例如,現在 example.com 可以向 www.example2.com 提交請求了:
var xhr = new XMLHttpRequest(); // xhr.withCredentials = true; //如果需要Cookie等 xhr.open('GET', 'http://www.example2.com/hello.json'); xhr.onload = function(e) { var data = JSON.parse(this.response); ... } xhr.send();
cors在移動終端支持的不錯,可以考慮在移動端全面嘗試;PC上有不兼容和沒有完美支持。
CORS提供了一種跨域請求方案,但沒有為安全訪問提供足夠的保障機制,如果你需要信息的絕對安全,不要依賴CORS當中的權限制度,應當使用更多其它的措施來保障。
七.Flash
利用flash的URLLoader,也可以輕松實現跨域數據交互。只要站點B的跨域策略文件(crossdomain.xml,在站點根目錄下放置)中包含了站點A,A站就可以獲取B站的數據,提交數據給B站。crossdomain.xml的用法。
我們可以把JS和flash的交互封裝一下,更方便的使用。這里有一個別人封裝好的版本,使用起來和原生的XMLHttpRequest幾乎一模一樣:
var req; function callback() { if (req.readyState == 4) { try { if (req.status != 200) { alert('error detected 1'); } else { alert("got data: "+req.responseText); } } catch(e) { alert('error detected 2'); } } } function test_get() { req = new CrossXHR(); req.onreadystatechange = callback; req.open('GET', 'http://www.pliantdev.com/support/test.xml'); req.send(); }
附:后台proxy代碼
發起方頁面代碼如下:
<form id="form1" runat="server"> <div> <input type="text" id="txtSrc" value="http://www.gzsums.edu.cn/webclass/html/html_design.html" style="width: 378px" /> <input id="btnProxy" type="button" value="通過Proxy獲取數據" onclick="GetDataFromProxy();" /><br /> <br /> <br /> </div> <div id="divData"></div> </form> </body> <script language="javascript" type="text/javascript"> function GetDataFromProxy() { var src = document.getElementById('txtSrc').value; var request = null; if (window.XMLHttpRequest) { request = new XMLHttpRequest(); } else if (window.ActiveXObject) { request = new ActiveXObject("Microsoft.XMLHTTP"); } request.onreadystatechange = function() { var ready = request.readyState; var data = null; { if (ready == 4) { data = request.responseText; document.getElementById('divData').innerHTML = data; } else { document.getElementById('divData').text = "Loading"; } } } var url = "Proxy.ashx?src=" + escape(src); request.open("get",url,false); request.send(null); } </script>
發起方Proxy代碼如下:
using System.Data; using System.Linq; using System.Web; using System.Web.Services; using System.Web.Services.Protocols; using System.Xml.Linq; using System.IO; using System.Net; using System.Text; namespace WebApplication1 { /// <summary> /// Summary description for $codebehindclassname$ /// </summary> [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class Proxy : IHttpHandler { const int BUFFER_SIZE = 8 * 1024; public void ProcessRequest(HttpContext context) { context.Response.ContentType = "text/plain"; string src = context.Request["src"]; WebRequest wr = WebRequest.Create(src); WebResponse wres = wr.GetResponse(); Encoding resEncoding = System.Text.Encoding.GetEncoding("gb2312"); StreamReader sr = new StreamReader(wres.GetResponseStream(), resEncoding); string html = sr.ReadToEnd(); sr.Close(); wres.Close(); context.Response.Write("<br/><br/><br/><br/>"); context.Response.Write(html); } public bool IsReusable { get { return false; } } } }
-------------------------------------------------------------------------------------------------------------------------------------
完
轉載需注明轉載字樣,標注原作者和原博文地址。