也談談同源策略和跨域問題
所謂同源策略,指的是瀏覽器對不同源的指令碼或者文字的訪問方式進行的限制。比如源a的js不能讀取或設定引入的源b的元素屬性。
那麼先定義下什麼是同源,所謂同源,就是指兩個頁面具有相同的協議,主機(也常說域名),埠,三個要素缺一不可。
可以看下面的幾個示例來更加清楚的瞭解一下同源的概念:
URL1 | URL2 | 說明 | 是否允許通訊 |
---|---|---|---|
http://www.foo.com/js/a.js | http://www.foo.com/js/b.js | 協議、域名、埠都相同 | 允許 |
http://www.foo.com/js/a.js | http://www.foo.com:8888/js/b.js | 協議、域名相同,埠不同 | 不允許 |
https://www.foo.com/js/a.js | http://www.foo.com/js/b.js | 主機、域名相同,協議不同 | 不允許 |
http://www.foo.com/js/a.js | http://www.bar.com/js/b.js | 協議、埠相同,域名不同 | 不允許 |
http://www.foo.com/js/a.js | http://foo.com/js/b.js | 協議、埠相同,主域名相同,子域名不同 | 不允許 |
同源策略限制了不同源之間的互動,但是有人也許會有疑問,我們以前在寫程式碼的時候也常常會引用其他域名的js檔案,樣式檔案,圖片檔案什麼的,沒看到限制啊,這個定義是不是錯了。其實不然,同源策略限制的不同源之間的互動主要針對的是js中的XMLHttpRequest等請求,下面這些情況是完全不受同源策略限制的。
- 頁面中的連結,重定向以及表單提交是不會受到同源策略限制的。連結就不用說了,導航網站上的連結都是連結到其他站點的。而你在域名
www.foo.com
下面提交一個表單到www.bar.com
是完全可以的。 - 跨域資源嵌入是允許的,當然,瀏覽器限制了Javascript不能讀寫載入的內容。如前面提到的嵌入的
<script src="..."></script>,<img>,<link>,<iframe>等
。當然,如果要阻止iframe嵌入我們網站的資源(頁面或者js等),我們可以在web伺服器加上一個X-Frame-Options DENY
頭部來限制。nginx就可以這樣設定add_header X-Frame-Options DENY;
。
既然有這麼多的情況是沒有同源策略限制的,那麼通常的跨域問題從何而來呢?轉到下一節跨域問題。
2 跨域問題
這一節來討論下跨域問題,當然前置條件是我們在WEB伺服器或者服務端指令碼中設定ACCESS-CONTROL-ALLOW-ORIGIN
頭部,如果設定了這些頭部並允許某些域名跨域訪問,則瀏覽器就會跳過同源策略的限制返回對應的內容。此外,如果你不是通過瀏覽器去獲取資源,比如你通過一個python指令碼去呼叫介面或者獲取js檔案,也不在這個限制之內。
2.1 Ajax跨域
通過ajax呼叫其他域的介面會有跨域問題。比如下面的例子,我在http://www.foo.com/index.html
中通過ajax呼叫請求http://www.bar.com/js/test.js
頁面,此時是會報錯的。
XMLHttpRequest cannot load http://www.bar.com/js/test.js.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'http://www.foo.com' is therefore not allowed access.
這是因為我們的WEB伺服器沒有設定ACCESS-CONTROL-ALLOW-ORIGIN
頭部,預設情況下是禁止跨域引用資源的。當然這裡有一點要注意,其實這個跨域請求是傳送成功了的,伺服器也有返回test.js內容,只是瀏覽器禁止Javascript去取response的資料而已。如果要設定ACCESS-CONTROL-ALLOW-ORIGIN頭部,nginx可以使用下面的程式碼
add_header 'Access-Control-Allow-Origin' 'http://www.foo.com';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET,POST';
另外,我們看到直接通過Javascript去取iframe中的元素也是會報錯的,因為域名不同。報錯如下所示
Uncaught SecurityError: Failed to read the 'contentDocument' property from 'HTMLIFrameElement': Blocked a frame with origin "http://www.foo.com" from accessing a frame with origin "http://foo.com".
The frame being accessed set "document.domain" to "foo.com", but the frame requesting access did not. Both must set "document.domain" to the same value to allow access.
這時因為我們的主域名相同,因此可以在index.html和test.html中設定document.domain='foo.com'
來訪問iframe中的元素。注意,是兩個域名下面的檔案都要設定,即便是同樣的主域名。當然這是特例,如果是兩個完全不同的域名,是沒有辦法的,你不能把www.foo.com
的domain
設定成www.163.com
。
此外,在index.html裡面也可以看到通過<script>,<iframe>
等標籤都是可以跨域內嵌資源的。
# index.html
<!DOCTYPE html>
<html>
<head>
<title>test cross domain</title>
<script src="/js/jquery.js"></script>
<script src="http://www.bar.com/js/test.js"></script>
<script>
$(function(){
document.domain = 'foo.com'; //1 註釋掉則會報錯
var ifr = document.getElementById("testframe");
ifr.onload = function(){
var doc = ifr.contentDocument || ifr.contentWindow.document;
alert(doc.getElementsByTagName("h1")[0].childNodes[0].nodeValue);
}
});
$.ajax("http://www.bar.com/js/test.js"); //2 報錯
</script>
</head>
<body>
<h1>Test Cross Domain</h1>
<iframe id="testframe" src="http://foo.com/test.html"></iframe>
</body>
</html>
當然還可以通過iframe,location.hash,window.name,HTML5的postMessage
等方法來實現跨域資源訪問,更多內容參見Rain Man
的這篇文章 JavaScript跨域總結和解決辦法。
2.2 JSONP跨域訪問
JSONP也是開發中常見到的內容,在jquery中就有封裝,通過ajax請求多帶上一個jsonp的引數即可。JSONP也能夠實現跨域訪問資源,但是它的實現原理其實跟ajax沒有多少關係,它是通過動態插入<script>
標籤來實現跨域資源訪問的,因為根據前面內容我們已經知道,嵌入跨域資源瀏覽器是允許的。
下面通過一個簡單的例子來說明JSONP的原理。
兩個檔案,第一個是http://www.foo.com/jsonp.html
,通過動態建立script標籤載入了http://www.bar.com/js/outer.js
檔案,然後outer.js檔案返回的內容正好是一個函式呼叫,如此,實現了資料傳遞和回撥過程。當然,實際的jsonp介面中,會讓你傳一個函式名過去,然後返回的資料中回撥函式名就是你傳的函式名,回撥函式的引數則是封裝的json格式。jQuery中的jsonp實現原理基本就是這樣,更詳細的jsonp原理可以參見這篇大作深入淺出JSONP。
# jsonp.html
<script type="text/javascript">
function callback(data) {
alert(data.message);
}
function addScriptTag(src){
var script = document.createElement('script');
script.src = src;
document.body.appendChild(script);
}
window.onload = function(){
addScriptTag("http://www.foo.com/js/outer.js");
}
</script>
# outer.js
callback({message:"success"});