同源策略(Same-origin policy)約束了兩個域之間資源的加載方式,是一個很重要的安全機制用來隔離那些有潛在安全隱患的文檔。javascript
一個源由一個URL的協議(protocol)、主機(host)和端口(port)進行定義。若是兩個頁面擁有相同的協議、主機和端口一致的話,咱們就能夠稱它們爲同源。下面的表格比較了不一樣的URL跟http://store.company.com/dir/page.html
這個URL的同源狀況:html
URL | 是否同源 | 理由 |
---|---|---|
http://store.company.com/dir2... | 是 | |
http://store.company.com/dir/... | 是 | |
https://store.company.com/sec... | 否 | 協議不一樣 |
http://store.company.com:81/d... | 否 | 端口不一樣 |
http://news.company.com/dir/o... | 否 | 主機不一樣 |
同源策略控制了兩個源之間的交互,例如你使用XMLHttpRequest
發起一個請求,或者使用<img>
元素加載一張圖片,就會產生兩個源之間的交互。而當兩個源不相同時,有些交互會被容許,而有些則不被容許。而不容許的狀況,就是咱們常說的跨域訪問問題。那什麼狀況下不一樣源的交互會被容許,什麼狀況下又不被容許呢?大體能夠分爲以下的狀況:java
連接、跳轉和表單提交這些在跨域的狀況下都是被容許的。例如調用支付寶接口進行支付就是典型的跨域表單提交的場景,這種跨域的調用是被容許的。可是這個很容易被利用來進行CSRF
攻擊,因此咱們的表單提交須要作好這方面的防禦。ajax
跨域的資源內嵌是被容許的。下面是一些資源內容的例子:json
使用<script src="..."></script>
加載Javascript。只有同源的腳本在語法錯誤時會顯示錯誤信息。後端
使用<link rel="stylesheet" href="...">
加載CSS。跨源的CSS文件要求使用正確的Content-Type
響應頭。api
使用<img>
加載圖片。跨域
使用<video>
和<audio>
加載媒體文件。瀏覽器
使用 <object>
、<embed>
和<applet>
加載插件。安全
使用 @font-face
加載字體。有些瀏覽器容許加載跨域的字體,有些則不容許。
使用 <frame>
和<iframe>
加載任何東西。
跨域文檔間使用Javascript腳本進行交互,API的訪問有限制。例如使用ifame嵌入的頁面或者使用window.open
打開的頁面,若是跟父頁面不一樣源,則想經過Javascript去操做父頁面的DOM,是不容許的(反過來亦然)。
舉個例子,假設有這麼一個頁面http://www.example.com/index.html
:
<!DOCTYPE html> <html> <head> ..... </head> <body > <iframe src="http://sub.example.com/iframe.html" frameborder="0"></iframe> </body>
而後在http://sub.example.com/iframe.html
頁面對父頁面的背景色進行修改:
<!DOCTYPE html> <html> <head> ...... </head> <body > <script> window.parent.document.body.style.backgroundColor = 'green'; </script> </body>
因爲兩個頁面不一樣源,因此子頁面對父頁面的操做被禁止,例如在Firefox上你會看到如下的報錯:
Error: Permission denied to access property "document"
不一樣源之間的XMLHttpRequest
調用(也就是咱們常說ajax調用)是不被容許的,這個也是咱們最常遇到的跨域訪問場景。例如咱們在http://example.com/index.html
頁面進行以下的ajax調用:
var xhr = new XMLHttpRequest(); var url = 'http://otherexample.com/api/get-data'; xhr.open('GET', url, true); xhr.onreadystatechange = handler; xhr.send();
在Firefox下會報以下錯誤:
已攔截跨源請求:同源策略禁止讀取位於 http://otherexample.com/api/get-data 的遠程資源。(緣由:CORS 頭缺乏 'Access-Control-Allow-Origin')。
一個頁面的源是能夠修改的,修改的方法很簡單,就是經過Javascript腳本設置document.domain
的值。舉個例子,咱們在頁面http://store.company.com/dir/other.html
執行下面的代碼:
document.domain = 'company.com';
那麼這個頁面的域將會由store.company.com
變成company.com
,後面在判斷是否同源的時候,主機將會使用company.com
這個值,而不是store.company.com
。
那是否是修改域後就能跟同域的頁面進行交互呢。答案是否認的。例如,若是你在頁面中經過iframe嵌入http://company.com/dir/page.html
這個頁面,而後經過javascript去跟這個頁面交互,你會發現瀏覽器會報錯,就跟咱們以前那個例子同樣。按照上面的源的定義,這時候兩個頁面應該是同源的,爲何呢?由於修改document.domain
會致使端口號被設爲null
。因此另一個頁面也須要把document.domain
修改成相同的值,這樣兩個頁面的主機和端口就一致了,能夠進行互相的訪問了。
既然源能夠修改,那麼是否是就解決了咱們的跨域問題呢?明顯沒這麼簡單。首先,這種方法是有很大限制條件的,document.domain
這個值只能修改成這個頁面的當前域或者當前域的超級域。因此,這個方法只能解決同一超級域下的頁面跨域問題。其次,它的使用場景也頗有限,由於它須要頁面執行Javascript腳本,因此也就是說通常只能應用於頁面跟頁面的交互,例如訪問ifame頁面或者window.open
打開的頁面等等。因此若是你想用來解決ajax
之類的跨域調用,這個方法就無能爲力了。
使用代理也是解決跨域訪問的一個方法。上面修改document.domain
的方法只能用來訪問子域名的頁面,沒法訪問不一樣域的頁面,而使用代理則沒有這個問題。
例如咱們有一個頁面http://example.com/
,須要訪問http://otherexample.com/
這個頁面,咱們不直接對這個頁面進行訪問,而是經過請求另一個同源的頁面,這個頁面在後端經過代理服務器把請求轉發到http://otherexample.com/
,獲取數據並返回給客戶端。
另外,這個方法一樣能夠用於解決ajax
的跨域訪問問題。
JSONP
也被常常用來解決ajax
的跨域調用問題。JSONP
請求並非經過XMLHttpRequest
發起,而是使用<script>
進行調用。前面說過,內嵌資源通常不受同源政策影響,因此<script>
能夠加載其餘源的資源。
舉個例子,假設http://www.example.com/
頁面,想異步調用http://www.otherexample.com/ajax.json
這個接口,這個接口會返回以下的數據:
{ "id": "123", "name": "Captain Jack Sparrow" }
若是咱們經過XMLHttpRequest
發起調用,就會由於同源政策而失敗。因此咱們經過<script>
進行調用,並經過參數傳遞咱們的回調函數名:
<script src="http://www.otherexample.com/ajax.json?callback=myFunction"></script>
而後接口獲取到callback函數名後,把原來返回的數據做爲函數的參數,最終返回以下的Javascript:
myFunction({"id": "123", "name": "Captain Jack Sparrow"});
而後myFunction
就會執行,達到了調用的目的。
這個方法在大多數狀況下都頗有用,可是它也有它的侷限。一是它須要後端的配合,由於後端的接口須要根據約定的參數獲取回調函數名,而後跟返回數據進行拼接,最後進行響應。二是它只能進行異步的調用,由於它的原理是經過動態生成<script>
加載JS的方法,而這個過程是異步的,因此若是你想進行同步的調用,那麼這個方法就無能爲力了。
Web Messaging
(又稱cross-document messaging
)是HTML5的一個接口,容許兩個不一樣源的文檔之間進行通訊。
它主要用到了接口裏的postMessage
方法,這個方法能夠把純文本消息從一個域發送到另一個域。消息能夠發送如下的對象:
發送方文檔裏frame
和iframe
。
發送方經過Javascript打開的頁面。
發送方的父頁面。
打開發送方頁面的頁面。
消息event
包含了如下的屬性:
data
:收到的消息。
origin
:發送方的源,包括協議、主機名和端口。
source
:發送方的window
對象。
舉個例子,假設example.net
下的文檔A跟文檔裏用iframe加載的example.com
下的文檔B進行通訊,咱們向文檔B發送消息Hello B
,Javascript代碼大體以下:
var o = document.getElementsByTagName('iframe')[0]; o.contentWindow.postMessage('Hello B', 'http://example.com/');
咱們先獲取到文檔B的contentWindow
對象,而後把須要發送到消息以及文檔B的源傳給postMessage
。文檔B則經過監聽message
事件,捕獲到事件,並做相應的處理:
function receiver(event) { if (event.origin == 'http://example.net') { if (event.data == 'Hello B') { event.source.postMessage('Hello A, how are you?', event.origin); } else { alert(event.data); } } } window.addEventListener('message', receiver, false);
須要注意的是,postMessage
是個非阻塞的調用,也就是說是異步的。
Web Messaging
主要用於跨域文檔間的通信,因此它不能用來解決全部跨域調用的問題,例如ajax
調用。並且IE瀏覽器對它的支持也很有限。
CORS
(Cross-Origin Resource Sharing)是W3C提出的一個用於服務器端控制數據跨域傳輸的一個機制。 它的原理是經過一些新增長的HTTP頭讓服務端能定義哪些源的請求能夠被容許。
簡單舉個例子,假設咱們在頁面http://example.com/
發起一個跨域的XMLHttpRequest
請求:
var xhr = new XMLHttpRequest(); var url = 'http://otherexample.com/api/get-data/'; xhr.open('GET', url, true); xhr.onreadystatechange = handler; xhr.send();
正常狀況這個請求是不容許的。可是若是咱們在服務端返回如下響應頭:
Access-Control-Allow-Origin: *
這個Access-Control-Allow-Origin
頭表示服務端容許哪些源的請求,*
表示容許全部的源,因此上面的請求就被容許了。固然正常狀況下咱們不會這樣作,咱們須要把Access-Control-Allow-Origin
設置爲真正想容許的源。在請求頭會有一個叫Origin
的頭,它的值就是請求方的源(例如上面的請求會有Origin: http://example.com
這個請求頭),服務端應該根據這個頭去返回相應的Access-Control-Allow-Origin
頭。
固然CORS
的實際使用會比上面的例子複雜得多,具體能夠參考MDN的這篇文章和W3C的規範。
CORS
能夠說是解決XMLHttpRequest
跨域調用的一個比較好的方法,但IE瀏覽器對它的支持一樣很有限,直到IE11才徹底支持,因此在移動端更能發揮它的做用。