同源策略/SOP(Same origin policy)是一種約定,它是瀏覽器最核心也最基本的安全功能,若是缺乏了同源策略,瀏覽器很容易受到XSS、CSFR等攻擊(能夠參考個人這篇文章)。javascript
SOP要求兩個通信地址的協議、域名、端口號必須相同,不然兩個地址的通信將被瀏覽器視爲不安全的,並被block下來。好比「http頁面」和「https頁面」屬於不一樣協議;「qq.com」、「www.qq.com」、「a.qq.com」都屬於不一樣域名(或主機);「a.com」和「a.com:8000」屬於不一樣端口號。這三種狀況常規都是沒法直接進行通信的。html
咱們很容易模擬不一樣源的環境,用iframe來幫忙便可:前端
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>模擬跨域</title> </head> <body> <iframe src="http://baidu.com"></iframe> <script> window.frames[0].onload = function () { alert("1"); } </script> </body> </html>
上述代碼在chrome中會輸出blocking信息:java
即咱們沒法監聽百度首頁文檔onload的事件,由於top窗口跟iframe窗體是不一樣源的。node
現代瀏覽器的確在安全性上下了很多功夫,除了上述提到的默認禁止非同源頁面通信,還新增了CSP(Content Security Policy)報頭特性等安全限制功能。不過既然爲了用戶安全而關閉了一扇窗戶,天然也會爲開發者開啓一扇便利的窗戶,要突破SOP的限制,咱仍是有很多辦法和花樣的。ios
目錄web
CORSajax
XDRchrome
HTML5解決方案
1. Cross-document messaging
2. WebSocketnpm
iframe形式
1. document.domain
2. location.hash
3. window.name
其它形式
1. 服務器代理
2. flash socket
CORS
同域安全策略CORS(Cross-Origin Resource Sharing)是W3C在05年提出的跨域資源請求機制,它要求當前域(常規爲存放資源的服務器)在響應報頭添加Access-Control-Allow-Origin標籤,從而容許指定域的站點訪問當前域上的資源。咱們使用node/iojs來模擬一下(不懂node/iojs?不急,先看下個人入門文章):
服務器端:
require("http").createServer(function(req,res){ //報頭添加Access-Control-Allow-Origin標籤,值爲特定的URL或「*」 //「*」表示容許全部域訪問當前域 res.setHeader("Access-Control-Allow-Origin","*"); res.end("OK"); }).listen(1234);
客戶端:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>CORS</title> <script src="jq.js"></script> </head> <body> <div>catching data...</div> <script> $.ajax({ url:"http://127.0.0.1:1234/", success:function(data){ $("div").text(data) } }) </script> </body> </html>
運行客戶端頁面後,便能看到div內容成功變爲服務端發來的「OK」,實現了兩個不一樣域的頁面間的通信。經過上述代碼咱們也發現,CORS主要是在服務端上的實現(也不外乎是添加一個報頭標籤),客戶端的實現跟常規的請求沒啥出入。
不過CORS默認只支持GET/POST這兩種http請求類型,若是要開啓PUT/DELETE之類的方式,須要在服務端在添加一個"Access-Control-Allow-Methods"報頭標籤:
服務端:
require("http").createServer(function(req,res){ res.setHeader("Access-Control-Allow-Origin","http://127.0.0.1"); res.setHeader( "Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, HEAD, PATCH" ); res.end(req.method+" "+req.url); }).listen(1234);
XDR
惱人的IE8-是不支持上述的CORS滴,不過不走尋常路的巨硬在IE8開始引入了XDR(XDomainRequest)新特性(IE11已經再也不支持該特性),它實現了CORS的部分規範,只支持GET/POST形式的請求。另外在協議部分只支持 http 和 https 。
在服務器端,依舊要求在響應報頭添加"Access-Control-Allow-Methods"標籤(這點跟CORS一致)。
在客戶端,DR對象的使用方法與XHR對象很是類似,也是建立一個XDomainRequest的實例,調用open()方法,再調用send()方法。但與XHR對象的open()方法不一樣,XDR對象的open()方法只接收兩個參數:請求的類型和URL,由於全部XDR請求都是異步執行的,不能用它來建立同步請求。
請求返回以後,會觸發load事件,相應的數據也會保存在responseText屬性中,以下所示:
var xdr = new XDomainRequest(); xdr.onload = function() { alert(xdr.responseText); }; xdr.onerror = function() { alert("一個錯誤發生了!"); }; xdr.open("get", "http://127.0.0.1:1234/"); xdr.send(null);
因爲XDR實在太過期,這裏不作太多介紹,瞭解下便可,更多細節請查閱msdn。
HTML5解決方案
1. Cross-document messaging
在 Cross-document messaging 中,咱們可使用 postMessage 方法和 onmessage 事件來實現不一樣域之間的通訊,其中postMessage用於實時向接收信息的頁面發送消息,其語法爲:
otherWindow.postMessage(message, targetOrigin);
otherWindow: 對接收信息頁面的window的引用。能夠是頁面中iframe的contentWindow屬性;window.open的返回值;經過name或下標從window.frames取到的值。
message: 所要發送的數據,string類型。
targetOrigin: 容許通訊的域的url,「*」表示不做限制。
咱們能夠在父頁面中嵌入不一樣域的子頁面(iframe實現,並且常規會把它隱藏掉),在子頁面調用 postMessage 方法向父頁面發送數據:
父頁面(http://localhost:10847/sop/a.html):
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>postMessage</title> </head> <body> <iframe style="display:none;" id="ifr" src="http://127.0.0.1:10847/sop/b.html"></iframe> <script type="text/javascript"> window.addEventListener('message', function(event){ // 經過origin屬性判斷消息來源地址 if (event.origin == 'http://127.0.0.1:10847') { alert(event.data); // 彈出從子頁面post過來的信息 } }, false); </script> </body> </html>
子頁面(http://127.0.0.1:10847/sop/b.html):
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>子頁面</title> </head> <body> <script type="text/javascript"> var ifr = window.parent; //獲取父窗體 var targetOrigin = 'http://localhost:10847'; // 若寫成 http://127.0.0.1:10847 則將沒法執行postMessage ifr.postMessage('這是傳遞給a.html的信息', targetOrigin); </script> </body> </html>
執行以下:
關於 Cross-document messaging 的更多細節可參考這篇文檔。
2. WebSocket
WebSocket protocol 是HTML5一種新的協議。它實現了瀏覽器與服務器全雙工通訊,同時容許跨域通信,是server push技術的一種很棒的實現。
咱們先簡單看下webSocket在客戶端上的api:
var ws = new WebSocket('ws://127.0.0.1:8080/url'); //新建一個WebSocket對象,注意服務器端的協議必須爲「ws://」或「wss://」,其中ws開頭是普通的websocket鏈接,wss是安全的websocket鏈接,相似於https。 ws.onopen = function() { // 鏈接被打開時調用 }; ws.onerror = function(e) { // 在出現錯誤時調用,例如在鏈接斷掉時 }; ws.onclose = function() { // 在鏈接被關閉時調用 }; ws.onmessage = function(msg) { // 在服務器端向客戶端發送消息時調用 // msg.data包含了消息 }; // 這裏是如何給服務器端發送一些數據 ws.send('some data'); // 關閉套接口 ws.close();
服務端這塊咱們繼續用node/iojs來編寫,並使用socket.io模塊輔助,socket.io很好地封裝了webSocket接口,提供了更簡單、靈活的接口,也對不支持webSocket的瀏覽器提供了向下兼容(例如替換爲Flash Socket/Comet)。
咱們先寫服務端,首先咱們得在項目根目錄下使用npm命令安裝好socket.io模塊:
npm install socket.io
接着新建服務端腳本(訪問地址是http://127.0.0.1:1234/):
var io = require('socket.io'); var server = require("http").createServer(function(req,res){ res.writeHead(200, { 'Content-type': 'text/html'}); }).listen(1234); io.listen(server).on('connection', function (client) { client.on('message', function (msg) { //監聽到信息處理 console.log('Message Received: ', msg); client.send('服務器收到了信息:'+ msg); }); client.on("disconnect", function() { //斷開處理 console.log("Server has disconnected"); }) });
客戶端頁面(http://localhost:10847/sop/a.html,注意使用了socket.io以後,接口跟原生的不太同樣了):
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>socket.io</title> <script src="jq.js"></script> <script src="https://cdn.socket.io/socket.io-1.3.4.js"></script> </head> <body> Incoming Chat: <ul></ul> <br/> <input type="text" /> <script> $(function () { var iosocket = io.connect('http://127.0.0.1:1234/'), $ul = $("ul"), $input = $("input"); iosocket.on('connect', function() { //接通處理 $ul.append($('<li>連上啦</li>')); iosocket.on('message', function(message) { //收到信息處理 $ul.append($('<li></li>').text(message)); }); iosocket.on('disconnect', function() { //斷開處理 $ul.append('<li>Disconnected</li>'); }); }); $input.keypress(function (event) { if (event.which == 13) { //回車 event.preventDefault(); iosocket.send($input.val()); $input.val(''); } }); }); </script> </body> </html>
客戶端頁面執行效果以下:
WebSocket能夠很好地擺脫無狀態的http鏈接,從而很好地處理鏈接斷開、數據錯誤的狀況,不過缺點是兼容性還不夠好,但咱可以使用上述的socket.io來向下兼容。
JSONP
這個實在用到爛大街了,提起跨域實現,其實最容易想到的就是它。JSONP(JSON with Padding)是JSON的一種「使用模式」,主要是利用script標籤不受同源策略限制的特性,向跨域的服務器請求並返回一段JSON數據。
常規先後端會約定好某個JSONP請求的callback名(好比隨便起個名字「abc」),服務端返回的JSON數據會被這個callback名包裹起來,進而方便服務器區分收到的請求,也方便客戶端區分其收到的響應數據。咱們能夠利用jQuery輕鬆實現JSONP:
客戶端(訪問地址http://localhost:10847/sop/a.html):
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>JSONP</title> <script src="jq.js"></script> </head> <body> <div></div> <script> $.ajax({ url:'http://127.0.0.1:1234/', dataType:"jsonp", //告知jQ咱們走的JSONP形式 jsonpCallback:"abc", //callback名 success:function(data){ console.log(data) } }); </script> </body> </html>
服務端(訪問地址http://127.0.0.1:1234/ ):
var http = require('http'); var urllib = require('url'); var data = {'name': 'vajoy', 'addr': 'shenzhen'}; http.createServer(function(req, res){ res.writeHead(200, { 'Content-type': 'text/plain'}); var params = urllib.parse(req.url, true); //console.log(params); if (params.query && params.query.callback) { //console.log(params.query.callback); var str = params.query.callback + '(' + JSON.stringify(data) + ')';//jsonp res.end(str); } else { res.end(JSON.stringify(data));//普通的json } }).listen(1234)
客戶端執行結果:
不過JSONP始終是無狀態鏈接,不能獲悉鏈接狀態和錯誤事件,並且只能走GET的形式。
iframe形式
在好久之前的石器時代,對於不支持 XMLHttpRequest 的瀏覽器的最佳回溯方法之一就是使用IFRAME對象,固然常規只是用它來實現流模式的Comet,而不是解決跨域通訊的問題。
使用iframe跨域其實有點劍走偏鋒的既視感,也存在一些限制性。下面均來介紹下。
1. document.domain
該方法只適合主域相同但子域不一樣的狀況,好比 a.com 和 www.a.com,咱們只須要給這兩個頁面都加上一句 document.domain = 'a.com' ,就能夠在其中一個頁面嵌套另外一個頁面,而後進行窗體間的交互。
爲了方便模擬環境,咱們修改下hosts文件:
127.0.0.1 a.com
127.0.0.1 www.a.com
這樣咱們訪問 a.com 的時候便能映射到本地了。
頁面a.html(訪問地址http://a.com:8080/sop/a.html):
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>iframe</title> <script src="jq.js"></script> </head> <body> <iframe src="http://www.a.com:8080/sop/b.html"></iframe> <script> document.domain = 'a.com'; $("iframe").load(function(){ $(this).contents().find("div").text("OK") }) </script> </body> </html>
頁面b.html(訪問地址http://www.a.com:8080/sop/b.html):
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>b.html</title> <script src="jq.js"></script> </head> <body> <div></div> <script> document.domain = 'a.com'; </script> </body> </html>
這時候咱們訪問a.html會發現b.html裏的內容被成功修改:
2. location.hash
location.hash/url hash 是個好東西,在以前咱們曾利用avalon前端路由來實現簡單的SPA頁面(這篇文章),即是助力於location.hash。
利用url地址改變但不刷新頁面的特性(在url: http://a.com#hello 中的 '#hello' 就是location.hash,改變hash並不會致使頁面刷新,因此能夠利用hash值來進行數據傳遞)和iframe,咱們能夠實現跨域傳遞簡單信息。
不過這個實現略麻煩,常規咱們會想,在a.html下嵌套一個不一樣域的b.html,而後 a 和 b 互相修改彼此的hash值,也不斷監聽本身的hash值,從而實現咱們的需求。惋惜的是,大部分瀏覽器不容許修改不一樣域的父窗體的hash值(parent.location.hash),也就是說a雖能修改b的hash值,但反過來由b修改a的hash值卻不成立。
爲了解除該限制,咱們能夠在b頁面中增長一個和a同域的iframe(c.html)來作代理,這樣b能夠修改c,而c能夠修改a(即修改parent.parent.location.hash,別忘了a和c同域哦)。下面直接模擬這三個頁面,作到讓b向a傳輸信息(固然本質上是b向c,c再向a傳輸):
a.html(訪問地址http://a.com:8080/sop/a.html):
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>iframe</title> <script src="jq.js"></script> </head> <body> <div></div> <iframe src="http://www.a.com:8080/sop/b.html" style="display: none;"></iframe> <script> var hash = ""; function checkHash() { var data = location.hash ? location.hash.substring(1) : hash; if (hash !== data) { $("div").text('hash變化爲:' + data); hash = data; } } setInterval(checkHash, 2000); </script> </body> </html>
b.html(訪問地址http://www.a.com:8080/sop/b.html):
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>b.html</title> </head> <body> <script> try { //有的瀏覽器(Firefox)仍是能夠直接操做parent.location.hash的 parent.location.hash = 'a=1&b=2'; } catch (e) { // ie、chrome的安全機制沒法修改parent.location.hash // 因此要利用一個代理iframe var ifrproxy = document.createElement('iframe'); ifrproxy.style.display = 'none'; ifrproxy.src = 'http://a.com:8080/sop/c.html#a=1&b=2'; //必須跟a.html同域 document.body.appendChild(ifrproxy); } </script> </body> </html>
c.html(訪問地址http://a.com:8080/sop/c.html):
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>c.html</title> <script src="jq.js"></script> </head> <body> <script> //由於parent.parent和自身屬於同一個域,因此能夠改變其location.hash的值 parent.parent.location.hash = self.location.hash.substring(1); </script> </body> </html>
訪問a.html後,效果以下:
成功傳遞了數據「a=1&b=2」。該方法優勢是兼容較好,缺點卻顯而易見——可傳遞的數據類型、長度均受限,數據仍是直接顯示在url上的,不夠安全。另外其實現也較麻煩,還要搞setInterval不斷監聽,跟輪詢沒區別了。
3. window.name
window.name 的美妙之處在於,窗體的name值在頁面跳轉後依舊存在、保持原值(即便跳轉的頁面不一樣域),而且能夠支持很是長的 name 值(2MB)。
若是咱們在a頁面須要和不一樣域的b頁面通訊,咱們能夠如今a頁面嵌入b頁面,待b頁面有數據要傳遞時,把數據附加到b頁面窗口的window.name上,而後把窗口跳轉到一個和a頁面同域的c頁面,這樣a就能輕鬆獲取到內嵌窗體(地址已由跨域的b變爲同域的c)的window.name了(若是須要,獲取到數據後再把c跳轉到b,並重復循環前面的步驟,同時a頁面以setInterval的形式來達到輪詢的效果)。咱們繼續模擬這三個頁面:
a.html(訪問地址http://a.com:8080/sop/a.html):
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>window.name</title> <script src="jq.js"></script> </head> <body> <div></div> <iframe src="http://www.a.com:8080/sop/b.html" style="display: none;"></iframe> <script> varifr = window.frames[0], loc = "", data = ""; function checkData(){ loc = ifr.location; if(loc.host){ //獲取到了,說明iframe已轉到同域的c頁面 if(ifr.name !== data){ //說明有新數據 data = ifr.name; $("div").text(JSON.parse(data).name); ifr.location = "http://www.a.com:8080/sop/b.html"; //數據收到後重回b頁面接收新數據 } }else return; } setInterval(checkData,2000); //每2秒輪詢一次 </script> </body> </html>
b.html(訪問地址http://www.a.com:8080/sop/b.html):
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>b.html</title> </head> <body> <script> window.name = '{"name":"vajoy","addr":"shenzhen"}'; location = "http://a.com:8080/sop/c.html"; //跳轉到和a同域的c頁面 </script> </body> </html>
c.html頁面啥都不用寫,純粹一個空的html便可,畢竟只是一個代理頁面罷了。
咱們訪問a頁面,會成功收到來自不一樣域b的數據:
其它形式
1. 服務器代理
頁面直接向同域的服務端發請求,服務端進行跨域處理或爬蟲後,再把數據返回給客戶端頁面。依舊用node/iojs來模擬服務端,下面的代碼來自木的樹的文章:
客戶端:
<!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"> <title>proxy_test</title> <script> var f = function(data){ alert(data.name); } var xhr = new XMLHttpRequest(); xhr.onload = function(){ alert(xhr.responseText); }; xhr.open('POST', 'http://localhost:8888/proxy?http://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer', true); xhr.send("f=json"); </script> </head> <body> </body> </html>
服務端:
var proxyUrl = ""; if (req.url.indexOf('?') > -1) { proxyUrl = req.url.substr(req.url.indexOf('?') + 1); console.log(proxyUrl); } if (req.method === 'GET') { request.get(proxyUrl).pipe(res); } else if (req.method === 'POST') { var post = ''; //定義了一個post變量,用於暫存請求體的信息 req.on('data', function(chunk){ //經過req的data事件監聽函數,每當接受到請求體的數據,就累加到post變量中 post += chunk; }); req.on('end', function(){ //在end事件觸發後,經過querystring.parse將post解析爲真正的POST請求格式,而後向客戶端返回。 post = qs.parse(post); request({ method: 'POST', url: proxyUrl, form: post }).pipe(res); }); }
2. flash socket
其實在前面介紹socket.io的時候就有提到,在不兼容WebSocket的瀏覽器下,socket.io會以flash socket或Comet的形式來兼容,而flash socket是支持跨域通訊的形式,跟WebSocket同樣走的TCP/IP套接字協議。具體的實現可參考Adobe官方文檔,本文不贅述。
至此便介紹了這麼幾種經常使用的跨域通訊的實現方式,但願對你能有所幫助。
大過年的還這麼辛苦寫文章我也是瘋了,anyway,祝各位新春快樂,新的一年揚眉吐氣、萬事順利!
共勉~