在跨域安全性方面,有多個地方會有限制,主要是XMLHttpRequest對象的跨域限制和iFrame的跨域限制,下面我們分別來看一下。
Ajax跨域(CORS)
CORS是一個W3C標准,全稱是"跨域資源共享"(Cross-origin resource sharing)。
它允許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。
CORS需要瀏覽器和服務器同時支持。目前,所有瀏覽器都支持該功能,IE瀏覽器不能低於IE10。
整個CORS通信過程,都是瀏覽器自動完成,不需要用戶參與。對於開發者來說,CORS通信與同源的AJAX通信沒有差別,代碼完全一樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感覺。
因此,實現CORS通信的關鍵是服務器。只要服務器實現了CORS接口,就可以跨源通信。
以PHP服務端為例
如果一個請求允許http://www.example.com的域請求的話,可以在PHP中這么寫:
header("Access-Control-Allow-Origin:http://www.example.com");
如果有多個允許跨域訪問的地址,可以添加多條或使用“,”號進行分隔,如果希望所有來源的地址都可以訪問當前的頁面,可以如下:
header("Access-Control-Allow-Origin:*");
同時,也可以控制請求的方法,如下是允許GET和POST兩種請求方法:
header("Access-Control-Allow-Method:POST,GET");
IE中,要跨域需要使用到XDomainRequest的對象,我們這里就不展開來說了,而在其它瀏覽器中,使用XMLHttpRequest對象即可,並沒有不一樣的地方。
一個需要注意的地方
最近在公司開發的過程中,遇見的情況是PHP已經添加了header("Access-Control-Allow-Origin:*");代碼,但是客戶端請求時,仍然報錯,最終查出的bug是,包含的其它PHP中,也添加了相同的代碼,導致服務端返回時,在Chrome的Network頁面中,發現有兩條一樣的頭部數據,去掉一條即可。
CORS跨域之前的跨域方法
在CORS標准出現之前,XHR對象是不能跨域請求的,不過開發人員憑借自己的聰明才智,繞過了XHR對象,創造了另外幾種可以跨域請求的方式,我們下面開簡單的看看。
圖像Ping
前端里面圖片元素沒有跨域的限制,所以我們可以通過模擬請求一個圖片的方法,向服務端發送數據,服務端對應的頁面可以返回一個簡單的圖片數據或者什么數據都不返回(客戶端得到204狀態)。
1 var img = new Image(); 2 img.onload = img.onerror = function () { 3 console.log("Done"); 4 } 5 img.src = "http://www.example.com/test.php?name=LiLei&age=28";
缺點是可以得到請求成功和失敗的回調但是得不到服務端返回的數據,除非將數據放入圖片返回。
統計在線廣告瀏覽量等不需要返回數據的情況下通常使用該方式。
JSONP
即JSON with padding的簡寫,簡單來說:就是通過添加一個script標簽,通過創建該標簽的src地址來傳遞參數,而服務端返回JS代碼的內容,JS代碼回調一個頁面中已經存在的JS方法,同時將需要給到客戶端的信息作為參數傳遞即可。這樣就可以繞過XHR對象實現跨域請求並得到服務端返回的數據。
1 function handleResponse (response) { 2 console.log("name: " + response.name + ", age: " + response.age); 3 } 4 5 var script = document.createElement("script"); 6 script.src = "http://www.example.com/test.php?name=LiLei&age=28"; 7 document.body.insertBefore(script, document.body.firstChild);
當前代碼中的handleResponse方法即服務端會回調的方法。
handleResponse({"name":"Han Meimei", "age":27});
服務端返回上面的文本后,由於是添加的script腳本,所以會調用到handleResponse方法並得到服務端的數據。
JSONP缺點
- 訪問的其他域如果不安全,可能會返回一些有害的JS代碼到當前頁面進行執行;
- 難以確定請求失敗的響應,需要用戶自己實現一個超時計時器。
iFrame跨域
JavaScript出於安全方面的考慮,不允許iFrame跨域訪問和調用其他頁面的對象。
試想一下,如果我們做了一個釣魚網站,使用一個iFrame引入了XX銀行的首頁,把自己偽裝成該銀行首頁,此時如果我們可以跨域調用和修改XX銀行的所有數據,也可以通過修改和注入JS代碼,后果就是:如果有人登錄了我們的假網站,我們就可以通過給這個iFrame添加我們自己的JS代碼來輕松獲得這個人的帳號和密碼信息。
所以,iFrame不允許跨域訪問是基於安全的考慮,也是必要的一個安全限制。
我們看下,具體的跨域限制:
- http://www.a.com/a.js http://www.a.com/b.js 同一域名下 允許
- http://www.a.com/lab/a.js http://www.a.com/script/b.js 同一域名下不同文件夾 允許
- http://www.a.com:8000/a.js http://www.a.com/b.js 同一域名,不同端口 不允許
- http://www.a.com/a.js https://www.a.com/b.js 同一域名,不同協議 不允許
- http://www.a.com/a.js http://70.32.92.74/b.js 域名和域名對應ip 不允許
- http://www.a.com/a.js http://script.a.com/b.js 主域相同,子域不同 不允許
- http://www.a.com/a.js http://a.com/b.js 同一域名,不同二級域名(同上) 不允許(cookie這種情況下也不允許訪問)
- http://www.cnblogs.com/a.js http://www.a.com/b.js 不同域名 不允許
但是不可否認的是,在某些情況下,我們還是希望不同域的頁面之間可以相互通信(注意這里不一定要相互可以調用修改)。下面我們來看看如何實現跨域的消息通信。
document.domain
通過修改多個框架domain屬性為同樣的值,可以突破跨域的限制,使多個頁面之間可以互相訪問到。
但是domain屬性的修改有下面兩個限制。
只能修改子域為主域,不能修改為其它域
域example.com嵌入了域p2p.example.com的頁面,通過修改p2p.example.com的domain即可實現雙方互相訪問和修改,如同沒有跨域一樣,但是要注意不能改為其它域:
1 document.domain = "example.com"; // 成功 2 document.domain = "baidu.com"; // 報錯
不能將主域修改為子域
如下,位於p2p.example.com的頁面:
1 document.domain = "example.com"; // 成功 2 document.domain = "p2p.example.com"; // 報錯
因為已經設置為主域了,再次設置會子域會報錯。
使用postMessage
跨文檔消息傳遞(cross-document messaging),簡稱XDM,在H5中該功能可以用來向iFrame嵌套的頁面和嵌套自己的頁面相互發送消息,也可以向當前頁面彈出的窗口相互傳遞消息。
通過XDM我們可以安全的實現頁面之間的跨域消息傳遞。
XDM得核心方法是postMessage,postMessage()方法允許來自不同源的腳本采用異步方式進行有限的通信,可以實現跨文本檔、多窗口、跨域消息傳遞。
postMessage(data,origin)方法接受兩個參數:
- data:要傳遞的數據,html5規范中提到該參數可以是JavaScript的任意基本類型或可復制的對象,然而並不是所有瀏覽器都做到了這點兒,部分瀏覽器只能處理字符串參數,所以我們在傳遞參數的時候需要使用JSON.stringify()方法對對象參數序列化,在低版本IE中引用json2.js可以實現類似效果。
- origin:字符串參數,指明目標窗口的源,協議+主機+端口號[+URL],URL會被忽略,所以可以不寫,這個參數是為了安全考慮,postMessage()方法只會將message傳遞給指定窗口,當然如果願意也可以建參數設置為"*",這樣可以傳遞給任意窗口。
而在發送了消息之后,符合條件(iFrame嵌套頁面或彈出窗口,且符合postMessage的origin參數的域)的其他頁面的window對象會收到該消息,作為“message”事件拋出該消息,對應的event對象有如下主要屬性:
- data:顧名思義,是傳遞來的message;
- source:發送消息的窗口對象;
- origin:發送消息窗口的源(協議+主機+端口號)。
下面我們看一個例子:
A.html
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 5 <title>A</title> 6 <body> 7 <p>A</p> 8 <input type="button" value="發送消息到frameB並得到frameB的回應" onclick="sendMsg()"> 9 <iframe id="frameB" src="./B.html" style="width: 90%;"></iframe> 10 <script type="text/javascript"> 11 window.addEventListener("message", function (event) { 12 console.log("A.html接收到消息:" + event.data); 13 }); 14 15 function sendMsg () { 16 var frameB = document.getElementById("frameB"); 17 frameB.contentWindow.postMessage("Hello I am Li Lei", "*"); 18 } 19 </script> 20 </body> 21 </html>
B.html
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 5 <title>B</title> 6 </head> 7 <body bgcolor="#ff0000"> 8 <p>B</p> 9 <script type="text/javascript"> 10 window.addEventListener("message", function (event) { 11 console.log("B.html接收到消息:" + event.data); 12 //向發送消息的window回送消息 13 event.source.postMessage("Hi, I am Han Meimei!", "*"); 14 }); 15 </script> 16 </body> 17 </html>
需要注意,window對象發送的消息只有自身可以接收到message消息。
crossOrigin屬性
服務端需要配置各種類型資源的跨域范圍權限,如Apache服務器可以如下配置:
1 <IfModule mod_setenvif.c> 2 <IfModule mod_headers.c> 3 <FilesMatch "\.(cur|gif|ico|jpe?g|png|svgz?|webp)$"> 4 SetEnvIf Origin ":" IS_CORS 5 Header set Access-Control-Allow-Origin "*" env=IS_CORS 6 </FilesMatch> 7 </IfModule> 8 </IfModule>
Node.js如下配置:
1 app.all('*',function (req, res, next) { 2 res.header('Access-Control-Allow-Origin', '*'); 3 res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild'); 4 res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS'); 5 if (req.method == 'OPTIONS') { 6 res.send(200); /讓options請求快速返回/ 7 } else { 8 next(); 9 } 10 });
crossOrigin屬性的值
該枚舉屬性指定在加載相關圖片時是否必須使用CORS。
- 當不設置該屬性時,資源將會不使用CORS加載(即不發送Origin:HTTP頭),這將阻止其在元素中進行使用。若設置了非法的值,則視為使用anonymous。
- "anonymous":會發起一個跨域請求(即包含Origin: HTTP頭)。但不會發送任何認證信息(即不發送cookie, X.509證書和HTTP基本認證信息)。如果服務器沒有給出源站憑證(不設置Access-Control-Allow-Origin: HTTP頭),這張圖片就會被污染並限制使用。
- "use-credentials":會發起一個帶有認證信息(發送cookie,X.509證書和HTTP基本認證信息)的跨域請求(即包含Origin:HTTP頭)。如果服務器沒有給出源站憑證(不設置Access-Control-Allow-Origin: HTTP頭),這張圖片就會被污染並限制使用。
script標簽
當我們引入一個不同源的js代碼時,不設置crossOrigin屬性,在執行上是沒有問題的,報錯在console面板也可以看到堆棧信息,但是在當前頁面使用window.onerror來抓取錯誤時,只能得到一個Script error的信息,沒有更多的信息了。
當我們為script標簽添加crossorigin="anonymous"屬性后,如果服務端沒有對跨域進行設置,會報如下錯誤:
Access to Script at 'http://127.0.0.1:3000/test.js' from origin 'http://192.168.2.34' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://192.168.2.34' is therefore not allowed access.
服務端進行允許跨域設定后,window.onerror可以抓取到詳細的信息了。
img標簽
我們在請求其它域下的圖片時,可以顯示出來,但是當調用toBlob(),toDataURL()和getImageData()等方法時會拋出安全錯誤,這是由於瀏覽器的安全策略導致的。
我們先看看下面的代碼:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 </head> 7 <body> 8 <canvas id='canvas' width='500px' height='500px'></canvas> 9 10 <script> 11 var image = document.createElement("img"); 12 // image.crossOrigin = "anonymous"; 13 image.onload = function() { 14 drawImg(image); 15 }; 16 image.onerror = function() { 17 console.log("load image error!"); 18 }; 19 image.src = "http://127.0.0.1:3000/images/img.png"; 20 21 function drawImg(img) { 22 var canvas = document.getElementById('canvas'); 23 var context = canvas.getContext("2d"); 24 context.drawImage(img, 0, 0, 100, 100); 25 26 var base64 = canvas.toDataURL('image/png'); 27 console.log(base64); 28 } 29 </script> 30 </body> 31 </html>
圖片不在當前域,可以繪制到canvas里顯示,但是調用toDataURL時會出現跨域的報錯,解決方法就是去掉注釋,為img標簽添加crossOrigin="anonymous"的屬性。
添加后,也需要服務端進行允許跨域設定才行哦,刷新后可以得到canvas中的圖片編碼信息,服務端不設定跨域會得到onerror的事件並且打印跨域異常信息。