瀏覽器同源策略,及跨域解決方案
一、Origin(源)
源由下面三個部分組成:
- 域名
- 埠
- 協議
兩個 URL ,只有這三個都相同的情況下,才可以稱為同源。
下來就以 “http://www.example.com/page.html” 這個連結來比較說明:
| 對比URL|結果 | 原因|
| ———— | ———— | ———— |
| http://m.example.com/page.html| 不同源|域名不同 |
| https://www.example.com/page.html| 不同源| 協議不同|
| http://www.example.com:8080/page.html| 不同源| 埠不同|
| http://www.example.com/page3.html|同源 | 同域名,同埠,同協議|
二、同源策略
瀏覽器的同源策略是一種安全功能,同源策略限制了從同一個源載入的文件或指令碼如何與來自另一個源的資源進行互動。這是一個用於隔離潛在惡意檔案的重要安全機制。所以a.com下的js指令碼採用ajax讀取b.com裡面的檔案資料是會報錯的。
三、哪些會受到同源策略限制
對於瀏覽器來說,除了DOM、Cookie、XMLHttpRequest 會受到同源策略的限制外,瀏覽器載入的一些第三方外掛也有各自的同源策略。最常見的一些外掛如 Flash ,有自己的控制策略。
所以,想要體驗下,同源策略限制,你就可以寫一個ajax 請求,比如127.0.0.1:80 要請求127.0.0.1:8080 的 a.js ;
127.0.0.1:80 裡的index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <h1>另一個頁面</h1> </body> <script> var xhr = new XMLHttpRequest(); xhr.open('get','http://127.0.0.1:8080/index.js'); xhr.send(null); xhr.onreadystatechange = function(){ if(xhr.readyState == 4 && xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ alert(xhr.responseText); } } </script> </html>
然後就會報錯了,出現了同源策略限制了。
四、什麼是跨域呢
說的跨域,其實呢就是跨源。而跨域是一個統稱,通過上面的我們知道了,因為同源策略,不同源之間,不能進行互動。那麼跨域就是解決不同源之間發起請求、請求資料、傳送資料、通訊等互動問題解決方法的統稱。
在瀏覽器中,<script>
、<img>
、<iframe>
、<link>
、<video>
等標籤都可以跨域載入資源,而不受同源策略的限制,通過 src 屬性載入的資源,瀏覽器都會發起一個 GET 請求,但是瀏覽器限制了 JavaScript 的許可權,使用js不能讀、寫載入的內容。
這句話什麼意思呢,其實就是,你可以通過這幾個標籤來跨域載入資源,但是,發起的GET請求 返回的資料,通過 js 獲取不到。
注意:通過<script>
標籤獲取 js 檔案裡的全域性屬性,方法等,可以通過 js 讀取到。是因為這些都是掛載在 window物件上的,看下面:
127.0.0.1 index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <div id="app"></div> <script type="text/javascript" src="http://127.0.0.1:8080/index.js"></script> <script type="text/javascript"> window.onload = function(){ say(); } </script> </body> </html>
127.0.0.1:8080 index.js
function say(){ var app = document.getElementById('app'); app.innerHTML = "我是被掛載到window物件上的方法,所以可以獲取到我!"; }
五、jsonp跨域
到底什麼是jsonp 跨域呢?其實,jsonp 跟 json 兩者沒有什麼關係,也沒有什麼相似的地方,大家都知道json 是一種資料格式,而jsonp 之所以被稱為jsonp,我認為跟它發出請求後,一般得到的,都是json格式資料有關吧。
上面說過了,<script>
、<img>
、<iframe>
、<link>
、<video>
這些標籤都可以發起跨域請求,其中的<script>
標籤都熟悉吧,經常用來載入 js 檔案。jsonp就是利用了這個標籤。
不知道大家有沒有疑問啊,既然這些標籤都能發起跨域請求,那麼為啥只用<script>
標籤可以請求到資料呢?其實呢,關鍵就在於,<script>
再請求得到資料後,遇到js程式碼,就會解析執行。理解這個也不難,你在js檔案裡寫的程式碼,肯定是要被執行的。
比如127.0.0.1
裡的index.html
頁面載入了一個<script src="index.js"></script>
:
function say(){ console.log("666"); } say();
當開啟127.0.0.1/index.html
頁面時,<script>
標籤發起了一個對index.js 的 GET 請求,得到資料後,js引擎開始解析執行,然後say
方法就被執行了,這時,控制檯就會輸出 “666”;
那麼jsonp就是利用了這點了。先來寫一個jsonp例項吧。
127.0.0.1 jsonp.html
:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <h1>JSONP</h1> </body> <script > function say(data){ alert(data); } </script> <script src="http://127.0.0.1:8080/index.php?callback=say"></script> </html>
然後是127.0.0.1:8080 index.php
檔案:
<?php $data = array( 'name' => 'zdx', 'sex' => 'man', 'age' => 18 ); $callback = $_GET['callback']; echo $callback . '(' . json_encode($data) . ')'; ?>
當訪問jsonp.html
時,其中的<script>
發起一個請求,併發送了一個名為callback
引數,值為字串"say"
。然後index.php
把傳進來的say
和要傳送的data
進行字串拼接,json_encode
函式就是把 資料轉成json 格式的。然後這個請求就返回了:say({"name":"zdx","sex":"man","age":18})
;然後<script>
得到這個資料後,就會解析執行say
函數了。
所以明白了吧,jsonp 是需要後端 支援的,需要配套使用,然後關於jsonp 是存在安全風險的,傳過來的資料直接執行,那麼只要改掉同名的函式,那麼想怎麼操作資料都可以了。還可以修改引數值,對傳到伺服器的資料進行修改,從而攻擊伺服器。
注意:此方法只能發起GET請求,通過jsonp傳送的請求,會隨帶 cookie 一起傳送。
六、CORS跨域(跨域資源共享)
CORS(Cross-Origin Resource Sharing,跨源資源共享)定義了在必須訪問跨源資源時,瀏覽器與伺服器應該如何溝通。CORS 背後的基本思想,就是使用自定義的 HTTP 頭部讓瀏覽器與伺服器進行溝通,從而決定請求或響應是應該成功,還是應該失敗。
注意:此方法IE8以下完全不支援,IE8-10部分支援。
這需要伺服器 和前端配合, 或者 後端和 前端配合。
可以看看阮老師的:ofollow,noindex">跨域資源共享 CORS 詳解
這裡以 php 為例,只需在需要被請求的 php 檔案里加上一個響應頭部header('Access-Control-Allow-Origin:http://127.0.0.1')
,後面的域名就是允許請求的域名。這裡就是表示允許來自http://127.0.0.1
所有的請求。
127.0.0.1:8080 index.php
:
<?php header('Access-Control-Allow-Origin:http://127.0.0.1'); echo "我是CORS跨域過來的!"; ?>
然後就是前端了。IE10及以上、Firefox 3.5+、Safari 4+、Chrome、iOS版 Safari和 Android平臺中的 WebKit都通過 XMLHttpRequest 物件實現了對 CORS 的原生支援。
127.0.0.1:80 index.html
:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <h1>另一個頁面</h1> </body> <script> var xhr = new XMLHttpRequest(); xhr.open('get','http://127.0.0.1:8080/index.php'); xhr.send(null); xhr.onreadystatechange = function(){ if(xhr.readyState == 4 && xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ alert(xhr.responseText); } } </script> </html>
而IE8 – IE9是通過XDR物件實現 CORS 的。
基於XDR的index.html
程式碼如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>XDR物件實現CORS</title> </head> <body> <h1>XDR物件實現CORS</h1> <script> var xdr = new XDomainRequest(); xdr.onload = function(){ console.log(xdr.responseText); }; xdr.open("get","http:127.0.0.1:8080/index.php"); xdr.send(null); </script> </body> </html>
注意:CORS可以發起 GET、POST請求,但是傳送的請求,預設不會隨帶 cookie 一起傳送, 也不會接受後端發過來的 cookie;
要想隨帶cookie 一起傳送。
需要在127.0.0.1:8080 index.php
新增header('Access-Control-Allow-Credentials:true');
頭部,然後在127.0.0.1:80 index.html
中var xhr = new XMLHttpRequest();
後面新增xhr.withCredentials = true;
七、document.domain 降域
同源策略認為域和子域屬於不同的域,如:
child1.a.com 與 a.com,
child1.a.com 與 child2.a.com,
xxx.child1.a.com 與 child1.a.com
兩兩不同源,可以通過設定 document.domain=’a.com’,瀏覽器就會認為它們都是同一個源。想要實現以上任意兩個頁面之間的通訊,兩個頁面必須都設定documen.damain=’a.com’。
此方式的特點:
1.只能在父域名與子域名之間使用,且將 xxx.child1.a.com域名設定為a.com後,不能再設定成child1.a.com。
2.存在安全性問題,當一個站點被攻擊後,另一個站點會引起安全漏洞。
3.這種方法只適用於 Cookie 和 iframe 視窗。
下面來模擬一下,在a.com 與 child1.a.com 之間通訊。如果要在本機測試,請自行更改host 等,訪問的都是本機80埠,這裡就不在累述了。
a.com index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <h1>主頁面</h1> <script> document.domain = 'a.com'; </script> <iframe src="http://child1.a.com/index1.html" frameborder="0"></iframe> </body> </html>
child1.a.com index.php
<?php echo "我是document.domain 降域過來的!"; ?>
child1.a.com index1.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <h1>child</h1> <script> document.domain = 'a.com'; var xhr = new XMLHttpRequest(); xhr.open('get','http://child1.a.com/index.php'); xhr.send(null); xhr.onreadystatechange = function(){ if(xhr.readyState == 4 && xhr.status >= 200 && xhr.status <= 300 || xhr.status == 304){ alert(xhr.responseText); } } </script> </body> </html>
注意:此方法可以發起 GET、POST 請求,發起的請求不會隨帶 cookie一起傳送,也不能接受後端發過來的 cookie
八、HTML5的postMessage方法
這是html5 新加的方法。
這個方法允許一個頁面的指令碼傳送資料到另一個頁面的指令碼中,不管指令碼是否跨域 。在一個window物件上呼叫postMessage()會非同步的觸發window上的onmessage事件,然後觸發定義好的事件處理方法。一個頁面上的指令碼仍然不能直接訪問另外一個頁面上的方法或者變數,但是他們可以安全的通過訊息傳遞技術交流。
比如說父頁面為127.0.0.1:80 的頁面,傳送資料給 127.0.0.1:8080 的子頁面:
127.0.0.1:80 index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <h1>父頁面</h1> <iframe id="iframe" src="http://127.0.0.1:8080/ty/index6.html" frameborder="0"></iframe> </body> <script> window.onload = function(){ var wd = document.getElementById('iframe').contentWindow; wd.postMessage('我是通過postMessage方法過來的!','http://127.0.0.1:8080'); } </script> </html>
127.0.0.1:8080 index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <h1>子頁面</h1> </body> <script> window.addEventListener("message", receiveMessage, false); function receiveMessage(event) { alert(event.data) } </script> </html>
然後訪問:127.0.0.1:80/index.html
,就得到想要的結果了,這方法通常用來進行兩個視窗通訊。
九、HTML5的Socket/">WebSocket
現代瀏覽器允許指令碼直連一個WebSocket地址而不管同源策略。然而,使用WebSocket URI的時候,在請求中插入Origin頭就可以標識指令碼請求的源。為了確保跨站安全,WebSocket伺服器必須根據允許接受請求的白名單中的源列表比較頭資料。
這個因為需要後端的支援,而且比較複雜,這裡就不舉例子了,感興趣的可以去查閱資料。
這裡貼一個阮老師的websocket教程吧:WebSocket 教程
十、window.name
window物件有一個name屬性,該屬性有一個特徵:即在一個視窗的生命週期內,視窗載入的所有的頁面都是共享一個window.name的,每一個頁面對window.name都有讀寫的許可權,window.name是持久的存在於一個視窗載入的所有頁面中的,並不會因為新的頁面的載入而被重置。
因此,就可以利用此特性,進行跨域通訊。
127.0.0.1:80 index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <bodyid="data"> <h1>window.name</h1> </body> <script type="text/javascript"> window.name = "我是document.name過來的資料。" location.href = "http://127.0.0.1:8080/ty/index8.html"; </script> </html>
127.0.0.1:8080 index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <script type="text/javascript"> alert(window.name) </script> </body> </html>
這時,訪問127.0.0.1:80/index.html
,跳轉到的127.0.0.1:8080/index.html
就能接受傳過來的資料了。
十一、location.hash
原理是利用location.hash來進行傳值。在url: http://a.com#helloword中的‘#helloworld’就是location.hash。
127.0.0.1:80 index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <bodyid="data"> <h1>window.name</h1> </body> <script type="text/javascript"> location.hash = "我是document.name過來的資料。" location.href = "http://127.0.0.1:8080/index.html" + location.hash; </script> </html>
127.0.0.1:8080 index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <script type="text/javascript"> alert(decodeURIComponent(location.hash.slice(1))); </script> </body> </html>
這時,訪問127.0.0.1:80/index.html
,跳轉到的127.0.0.1:8080/index.html
就能接受傳過來的資料了。
十二、proxy 跨域
這個完全是後端的實現,我就不說了,我也搞不懂,也沒意義。哈哈。