一、瀏覽器同源策略
首先我們需要了解一下瀏覽器的同源策略,關於同源策略可以仔細看看知乎上的一個解釋。傳送門
總之:同協議,domain(或ip),同端口視為同一個域,一個域內的腳本僅僅具有本域內的權限,可以理解為本域腳本只能讀寫本域內的資源,而無法訪問其它域的資源。這種安全限制稱為同源策略。
( 現代瀏覽器在安全性和可用性之間選擇了一個平衡點。在遵循同源策略的基礎上,選擇性地為同源策略"開放了后門"。 例如img script style等標簽,都允許垮域引用資源。)
下表給出了相對 http://store.company.com/dir/page.html 同源檢測的示例:
URL | 結果 | 原因 |
---|---|---|
http://store.company.com/dir2/other.html |
成功 | |
http://store.company.com/dir/inner/another.html |
成功 | |
https://store.company.com/secure.html |
失敗 | 協議不同 |
http://store.company.com:81/dir/etc.html |
失敗 | 端口不同(默認80) |
http://news.company.com/dir/other.html |
失敗 | 主機名不同 |
由於瀏覽器同源策略的限制,讓我們無法通過js直接在不同的域之間進行數據傳輸或通信,比如用ajax向一個不同的域請求數據,或者通過js獲取頁面中不同域的框架(iframe)中的數據。
二、jsonp實現跨域請求數據
在javascript中,我們不能直接用ajax請求不同域上的數據。但是,在頁面上引入不同域上的js腳本文件卻是可以的,jsonp正是利用這個特性來實現的。
本地測試利用PHP內置了的Web 服務器來模擬兩個端口不同的兩個域,現在在web_test目錄下有9000和9001兩個目錄,分別進入兩個目錄執行
web_test/9000: php -S 127.0.0.1:9000web_test/9001: php -S 127.0.0.1:9001執行后:
![]()
這時候開啟了兩個本地不同端口的服務器,現在在兩個目錄下的文件就是在兩個不同域。
在9001目錄下jsonp_test.html中
<!DOCTYPE html> <html> <head> <title>jsonp-test</title> </head> <body> <script type="text/javascript"> function callback_data (data) { console.log(data); } </script> <script type="text/javascript" src="http://127.0.0.1:9000/jsonp.php?callback=callback_data"></script> </body> </html>
可以看到我們在向9000目錄下的jsonp.php文件獲取數據時,地址后面跟了一個callback參數(一般的就是用callback這個參數名,你也可以用其他的參數名代替)。
如果你要獲取數據的頁面是你不能控制的,那你只能根據它所提供的接口格式進行獲取。
因為我們的type規定是當成是一個javascript文件來引入的,所以php文件返回的應該是一個可執行的js文件。
在9000目錄下jsonp.php中
<?php $callback = $_GET['callback']; // 獲取回調函數名 $arr = array("name" => "alsy", "age" => "20"); // 要請求的數據 echo $callback."(". json_encode($arr) .");"; // 輸出 ?>
頁面輸出就是這樣的:
callback_data({"name":"alsy","age":"20"}); //執行url參數中指定的函數,同時把我們需要的json數據作為參數傳入
這樣我們瀏覽器中輸入http://127.0.0.1:9001/jsonp_test.html,控制台打印出:
這樣我們就獲取到不同域中返回的數據了,同時jsonp的原理也就清楚了:
通過script標簽引入一個js文件,這個js文件載入成功后會執行我們在url參數中指定的函數,同時把我們需要的json數據作為參數傳入。所以jsonp是需要服務器端和客戶端相互配合的。
知道jsonp跨域的原理后我們就可以用js動態生成script標簽來進行跨域操作了,而不用特意的手動的書寫那些script標簽。比如jQuery封裝的方法就能很方便的來進行jsonp操作了。
9001目錄下的html中:
//$.getJSON()方法跨域請求 $.getJSON("http://127.0.0.1:9000/jsonp.php?callback=?", function(data){ console.log(data); });
原理是一樣的,只不過我們不需要手動的插入script標簽以及定義回掉函數。jQuery會自動生成一個全局函數來替換callback=?中的問號,之后獲取到數據后又會自動銷毀,實際上就是起一個臨時代理函數的作用。
從請求的url和響應的數據就可以很明顯的看出來了:
這里的 jQuery214036133305518887937_1462698255551 就是一個臨時代理函數。
$.getJSON方法會自動判斷是否跨域,不跨域的話,就調用普通的ajax方法;跨域的話,則會以異步加載js文件的形式來調用jsonp的回調函數。
另外jsonp是無法post數據的,盡管jQuery.getJSON(url, [data], [callback]); 提供data參數讓你可以攜帶數據發起請求,但這樣是以get方式傳遞的。比如:
9001目錄下的html中:
//$.getJSON()方法 $.getJSON("http://127.0.0.1:9000/jsonp.php?callback=?", {u:'abc', p: '123'}, function(jsonData){ console.log(jsonData); });
或者是調用$.ajax()方法指定type為post,它還是會轉成get方式請求。
9001目錄下的html中:
$.ajax({
type: 'post',
url: "http://127.0.0.1:9000/jsonp_post.php",
crossDomain: true,
data: {u: 'alsy', age: 20},
dataType: "jsonp",
success: function(r){
console.log(r);
}
});
以get形式的話是可以攜帶少量數據,但是數據量一大就不行了。
如果想post大量數據,就可以嘗試用CORS(跨域資源共享,Cross-Origin Resource Sharing)。傳送門
CORS定義一種跨域訪問的機制,可以讓AJAX實現跨域訪問。CORS 允許一個域上的網絡應用向另一個域提交跨域 AJAX 請求。實現此功能非常簡單,只需由服務器發送一個響應標頭即可。
即服務器響應頭設置
header('Access-Control-Allow-Origin: *'); // "*"號表示允許任何域向服務器端提交請求;也可以設置指定的域名,那么就允許來自這個域的請求: header('Access-Control-Allow-Methods: POST'); header('Access-Control-Max-Age: 1000');
比如:
9001目錄下的一個html文件:
$.ajax({ type: 'post', url: "http://127.0.0.1:9000/jsonp_post.php", crossDomain: true, data: {u: 'alsy', age: 20}, dataType: "json", success: function(r){ console.log(r); } });
9000目錄下的php文件:
<?php header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: POST'); header('Access-Control-Max-Age: 1000'); if($_POST){ $arr = array('name' => $_POST['u'], 'age' => $_POST['age']); echo json_encode($arr); } else { echo json_encode([]); } ?>
瀏覽器顯示:
這樣也是可以實現跨域post數據的。
- 兼容性。CORS是W3C中一項較新的方案,所以部分瀏覽器還沒有對其進行支持或者完美支持,詳情可移至 http://www.w3.org/TR/cors/。
- 安全問題。CORS提供了一種跨域請求方案,但沒有為安全訪問提供足夠的保障機制,如果你需要信息的絕對安全,不要依賴CORS當中的權限制度,應當使用更多其它的措施來保障。
三、iframe+window.domain 實現跨子域
現在9001目錄下有一個iframe-domain.html文件:
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>127.0.0.1:9001 -- iframe-domain</title> </head> <body> <h1>127.0.0.1:9001/iframe-domain.html</h1> <script>
function test(){ var obj = document.getElementById("iframe").contentWindow; //獲取window對象 console.log(obj); } </script> <iframe id="iframe" src="http://127.0.0.1:9000/domain.html" onload="test()"></iframe> </body> </html>
在這個頁面中存在一個不同域的框架(iframe),而iframe載入的頁面是和目標域在同一個域的,是能夠向目標域發起ajax請求獲取數據的。
那么就想能不能控制這個iframe讓它去發起ajax請求,但在同源策略下,不同域的框架之間也是不能夠進行js的交互。
雖然不同的框架之間(父子或同輩),是能夠獲取到彼此的window對象的,但是卻不能獲取到的window對象的屬性和方法(html5中的postMessage方法是一個例外,還有些瀏覽器比如ie6也可以使用top、parent等少數幾個屬性),總之就是獲取到了一個無用的window對象。
在這個時候,document.domain就派上用場了,我們只要兩個域的頁面的document.domain設置成一樣了,這個例子中由於端口不同,兩邊的document.domain也要重新設置成"127.0.0.1",才能正常通信。
要注意的是,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,因為主域已經不相同了。
通過設置document.domain為相同,現在已經能夠控制iframe載入的頁面進行ajax操作了。
9001目錄下的iframe-domain.html文件改為:
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>127.0.0.1:9001 -- iframe-domain</title> </head> <body> <h1>127.0.0.1:9001/iframe-domain.html</h1> <script> document.domain = '127.0.0.1'; //設置domain function test(){ var obj = document.getElementById("iframe").contentWindow; console.log(obj); obj.getData('http://127.0.0.1:9000/json_domain.php', '{u: "alsy-domain", age: "20"}', function(r){ console.log( eval("("+ r +")") ); }); } </script> <iframe id="iframe" src="http://127.0.0.1:9000/domain.html" onload="test()"></iframe> </body> </html>
9000目錄下有一個domain.html,和一個json_domain.php文件:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>domain</title> </head> <body> <h1>127.0.0.1/9000/domain.html</h1> <script> window.onload = function(){ document.domain = '127.0.0.1'; //設置domain window.getData = function(url, data, cb) { var xhr = null; if (window.XMLHttpRequest) { xhr = new XMLHttpRequest(); } else { xhr = new ActiveXObject("Microsoft.XMLHTTP"); } xhr.open('POST', url, true); xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { cb(xhr.responseText); } } xhr.send(data); } } </script> </body> </html>
<?php $str = file_get_contents('php://input'); echo json_encode($str); ?>
瀏覽器打印:
這樣就可以實現跨域,當然你也可以動態創建這么一個iframe,獲取完數據后再銷毀。
四、iframe+window.name 實現跨域
window對象有個name屬性,該屬性有個特征:即在一個窗口(window)的生命周期內,窗口載入的所有的頁面都是共享一個window.name的,每個頁面對window.name都有讀寫的權限。(window.name的值只能是字符串的形式,這個字符串的大小最大能允許2M左右甚至更大的一個容量,具體取決於不同的瀏覽器,一般夠用了。)
9001目錄下的iframe-window.name.html:
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>127.0.0.1:9001 -- iframe-window.name</title> </head> <body> <h1>127.0.0.1:9001/iframe-window.name.html</h1> <script> function test(){ var obj = document.getElementById("iframe"); obj.onload = function(){ var message = obj.contentWindow.name; console.log(message); } obj.src = "http://127.0.0.1:9001/a.html"; } </script> <iframe id="iframe" src="http://127.0.0.1:9000/window.name.html" onload="test()"></iframe> </body> </html>
9000目錄下的window.name.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>window.name</title> </head> <body> <h1>127.0.0.1/9000/window.name.html</h1> <script> //todo window.name = "This is message!"; </script> </body> </html>
瀏覽器輸出:
整個跨域的流程就是:現在9000/window.name.html中通過一些操作將數據存入window.name中了,而9001/iframe-window.name.html想要獲取到window.name的值就需要依靠iframe作為中間代理,首先把iframe的src設置成http://127.0.0.1:9000/window.name.html,這樣就相當於要獲取iframe的window.name,而要想獲取到iframe中的window.name,就需要把iframe的src設置成當前域的一個頁面地址"http://127.0.0.1:9001/a.html",不然根據前面講的同源策略,window.name.html是不能訪問到iframe里的window.name屬性的。
五、iframe+window.postMessage 實現跨域
html5炫酷的API之一:跨文檔消息傳輸。高級瀏覽器Internet Explorer 8+, chrome,Firefox , Opera 和 Safari 都將支持這個功能。這個功能實現也非常簡單主要包括接受信息的"message"事件和發送消息的"postMessage"方法。
發送消息的"postMessage"方法:
向外界窗口發送消息:
otherWindow.postMessage(message, targetOrigin);
otherWindow: 指目標窗口,也就是給哪個window發消息。
message: 要發送的消息,類型為 String、Object (IE8、9 不支持)
targetOrigin: 是限定消息接收范圍,不限制請使用 '*'
接受信息的"message"事件
var onmessage = function (event) { var data = event.data; var origin = event.origin; //do someing }; if (typeof window.addEventListener != 'undefined') { window.addEventListener('message', onmessage, false); } else if (typeof window.attachEvent != 'undefined') { //for ie window.attachEvent('onmessage', onmessage); }
回調函數第一個參數接收 event 對象,有三個常用屬性:
- data: 消息
- origin: 消息來源地址
- source: 源 DOMWindow 對象
9000目錄下的 postmessage.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>postmessage</title> </head> <body> <h1>127.0.0.1/9000/postmessage.html</h1> <script> window.onload = function () { if (typeof window.postMessage === undefined) { alert("瀏覽器不支持postMessage!"); } else { window.top.postMessage({u: "alsy"}, "http://127.0.0.1:9001"); } } </script> </body> </html>
9001目錄下的 iframe-postmessage.html:
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>127.0.0.1:9001 -- iframe-postmessage</title> </head> <body> <h1>127.0.0.1:9001/iframe-postmessage.html</h1> <script> function test(){ if (typeof window.postMessage === undefined) { alert("瀏覽器不支持postMessage!"); } else { window.addEventListener("message", function(e){ if (e.origin == "http://127.0.0.1:9000") { //只接收指定的源發來的消息 console.log(e.data); }; }, false); } } </script> <iframe id="iframe" src="http://127.0.0.1:9000/postmessage.html" onload="test()"></iframe> </body> </html>
瀏覽器打印:
六、說明
參考自 http://www.cnblogs.com/2050/p/3191744.html
如有錯誤或不同觀點請指出,共同交流。
完整代碼:傳送門


