關於跨域,有兩個誤區:javascript
1. ✕ 動態請求就會有跨域的問題html
✔ 跨域只存在於瀏覽器端,不存在於安卓/ios/Node.js/python/ java等其它環境java
2. ✕ 跨域就是請求發不出去了python
✔ 跨域請求能發出去,服務端能收到請求並正常返回結果,只是結果被瀏覽器攔截了jquery
之因此會跨域,是由於受到了同源策略的限制,同源策略要求源相同才能正常進行通訊,即協議、域名、端口號都徹底一致。ios
以下圖所示:nginx
這三個源分別因爲域名、協議和端口號不一致,致使會受到同源策略的限制。web
同源策略具體限制些什麼呢?ajax
1. 不能向工做在不一樣源的的服務請求數據(client to server)canvas
這裏有個問題以前也困擾了我好久,就是爲何home.com加載的cdn.home.com/index.js能夠向home.com發請求而不會跨域呢?其實home.com加載的JS是工做在home.com的,它的源不是提供JS的cdn,因此這個時候是沒有跨域的問題的,而且script標籤可以加載非同源的資源,不受同源策略的影響。
2. 沒法獲取不一樣源的document/cookie等BOM和DOM,能夠說任何有關另一個源的信息都沒法獲得 (client to client)
爲何會有同源策略呢?
1. 爲何要限制不一樣源發請求?
假設用戶登錄了bank.com,同時打開了evil.com,若是沒有任何限制,evil.com能夠向bank.com請求到任何信息,進而就能夠在evil.com向bank.com發轉帳請求等。
若是這樣,爲何不直接限制寫,只限制讀?
由於若是連請求都發不出去了,那就不能作跨域資源共享了,沒法讀取返回結果,evil.com就沒法繼續下一步的操做,如獲取轉帳請求的一些必要的驗證信息。
2. 爲何限制跨域的DOM讀取?
若是不限制的話,那麼很容易就能夠假裝其它的網站,如套一個iframe或者經過window.open的方法,從而獲得用戶的操做和輸入,如帳戶、密碼。
另外,添加這個http頭能夠限制別人把你的網站套成它的iframe:
X-Frame-Options: SAMEORIGIN
同源策略提供了安全的同時也形成了不方便,由於有時候咱們須要跨域請求,如獲取第三方提供的服務信息,因爲第三方的源和本網站的源不同,因此這個時候就受到跨域的限制。
跨域最經常使用的方法,應當屬CORS,以下圖所示:
只要瀏覽器檢測到響應頭帶上了CORS,而且容許的源包括了本網站,那麼就不會攔截請求響應。
CORS把請求分爲兩種,一種是簡單請求,另外一種是須要觸發預檢請求,這二者是相對的,怎樣纔算「不簡單」?只要屬於下面的其中一種就不是簡單請求:
(1)使用了除GET/POST/HEAD以外的請求方式,如PUT/DELETE
(2)使用了除Content-Type/Accept等幾個經常使用的http頭
這個時候就認爲須要先發個預檢請求,預檢請求使用OPTIONS方式去檢查當前請求是否安全,以下圖所示:
代碼裏面只發了一個請求,但在控制檯看到了兩個請求,第一個是OPTIONS,服務端返回:
返回頭裏麪包含了容許的請求頭、請求方式、源,以及預檢請求的有效期,上圖是設置了20天,在這個有效期內就不用再發一個options的請求,實際上瀏覽器有一個最長時間,如Chrome是5分鐘。若是在預檢請求檢測到當前請求不符合服務端設定的要求,則不會發出去了直接拋異常,這個時候就不用去發「複雜」的請求了。
如本源不在容許的源範圍內,則會拋異常,沒法獲取返回結果:
爲了支持CORS,nginx能夠這麼配:
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
}複製代碼
第二種經常使用的跨域的方法是JSONP,JSONP是利用了script標籤可以跨域,以下代碼所示:
function updateList (data) {
console.log(data);
}
$body.append(‘<script src=「http://otherdomain.com/request?callback=updateList"></script>');複製代碼
代碼先定義一個全局函數,而後把這個函數名經過callback參數添加到script標籤的src,script的src就是須要跨域的請求,而後這個請求返回可執行的JS文本:
// script響應返回的js內容爲
updateList([{
name: 'hello'
}]);複製代碼
因爲它是一個js,而且已經定義了upldateList函數,因此能正常執行,而且跨域的數據經過傳參獲得。這就是JSONP的原理。
因此因爲script/iframe/img等標籤的請求默認是能帶上cookie(cookie裏面帶上了登錄驗證的票token),用這些標籤發請求是可以繞過同源策略的,所以就能夠利用這些標籤作跨站請求僞造(CSRF),以下面代碼所示:
// 轉帳請求 <iframe src="http://Abank.com/app/transferFunds?amount=1500&destinationAccount=..."></iframe> // 配置路由器添加代理 <img src="http://192.168.1.1/admin/config/outsideInterface?nexthop=123.45.67.89" style="display:none">複製代碼
若是相應的網站支持GET請求,或者沒有作進一步的防禦措施,那麼若是用戶在另一個頁面登錄過了,再打開一個「有毒」的網站就中招了。
而動態ajax請求默認是不帶cookie的,若是你要帶cookie,能夠設置ajax的一個屬性withCredentials,以下代碼所示:
// 原生請求
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open("GET", "http://otherdomain.com/list");
xhr.send();
// jquery請求
$.ajax({
url: "http://otherdomain.com/list",
xhrFields: {
withCredentials: true
}
});
複製代碼
這個時候就和img/script標籤同樣,能帶上cookie,而且還支持除GET以外的其它方式。因此這種方式也是能實現CSRF的,以下圖所示:
因此若是轉帳請求只是不支持GET,沒作其它的防禦措施,仍然有CSRF攻擊的風險。那怎麼辦呢?
方法一是每次請求都要在參數裏面顯示地帶上token即登錄的票,雖然跨域請求能帶上cookie,可是經過document.cookie仍然是獲取不到其它源的cookie的,因此攻擊者沒法在代碼裏面拿到cookie裏面的token,因此就沒辦法了。方法一的缺點是會暴露token,因此須要帶token的最好不能是GET,由於GET會把參數拼在url裏面,用戶可能會無心把連接發給別人,但不知道這個連接帶上了本身的登錄信息。
方法二是每次轉帳請求前都先請求一個隨機串,這個串只能用一次轉帳或者支付請求,用完就廢棄,只有這個串對得上才能請求成功,攻擊者是沒法拿到這個串的,由於若是跨域請求帶cookie,瀏覽器要求Access-Control-Allow-Origin不能爲通配符,只能爲指定的源,如:
Access-Control-Allow-Origin: http://renren.com
add_header "Access-Control-Allow-Origin" "http://fedren.com";
add_header "Access-Control-Allow-Credentials" "true";複製代碼
關於cookie還有兩個地方值得注意,以下圖所示:
iframe訪問父頁面可經過window.parent獲得父窗口的window對象,經過open打開的能夠用window.opener,進而獲得父窗口的任何東西;父窗口若是和iframe同源的,那麼可經過iframe.contentWindow獲得iframe的window對象,若是和iframe不一樣源,則存在跨域的問題,這個時候可經過postMessage進行通信。
使用postMessage的基本原理以下圖所示:
// main frame
let iframeWin = document.querySelector("#my-iframe")
.contentWindow;
iframeWin.postMessage({age: 18}, "http://parent.com");
iframeWin.onmessage = function(event) {
console.log("recv from iframe ", event.data);
};
// iframe
window.onmessage = function(event) {
// test event.origin
if (event.origin !== expectOrigin) {
return;
}
console.log("recv from main frame ", event.data);
};
window.parent.postMessage("hello, this is from iframe ", "http://child.com");
複製代碼
以頁面嵌入youtobe視頻爲例,經過如下代碼能夠在頁面嵌入一個youtobe視頻,嵌入的是一個跨域的iframe,因此就涉及到如何和iframe進行通訊的問題。如怎麼知道iframe的狀態,觸發父頁面定義的事件onPlayerReady,這個是iframe通知父頁面,而父頁面能夠調player.stopVideo控制iframe的行爲,這個是父頁面通知iframe。
iframe通知父頁面是經過window.parent.postMessage,同時監聽message事件:
經檢查上面代碼4304行的c就是window.parent,這個embed-player.js是iframe的js,iframe的js經過postMessage發送了一個消息,如上圖右邊的窗口所示,而後在父窗口的widgetapi.js就收到了這個消息。
一樣地,父窗口的JS也是使用postMessage向iframe發送消息,以下圖所示:
固然postMessage不限於跨域,同域的也可使用,只是同域的話能夠經過window對象互相操做,你可能須要額外定義一些全局變量或者函數供其它frame使用,或者是定義一套事件機制(能夠藉助原生事件/jQuery/Vue事件等)。
這裏有一個特例,就是子域如mail.hello.com要跨hello.com的時候,能夠顯式地設置子域的document.domain值爲父域的domain:
document.domain = "hello.com";複製代碼
window.addEventListener('storage', function(e) {
e.key;
e.oldValue;
e.newValue;
e.url;
e.storageArea;
});
複製代碼
這個我沒試過,讀者能夠試一下。
再補充一點,websocket是不受同源策略限制的,沒有跨域的問題。CSS的字體文件是會有跨域問題,指定CORS就能加載其它源的字體文件(一般是放在cdn上的)。而canvas動態加載的外部image,也是須要指定CORS頭才能進行圖片處理,不然只能畫不能讀取。
最後,跨域分爲兩種,一種是跨域請求,另外一種訪問跨域的頁面,跨域請求能夠經過CORS/JSONP等方法進行訪問,跨域的頁面主要經過postMesssage的方式。因爲跨域請求不但能發出去還能帶上cookie,因此要規避跨站請求僞造攻擊的風險,特別是涉及到錢的那種請求。