關於跨域,有兩個誤區:
1. ✕ 動態請求就會有跨域的問題
✔ 跨域只存在於瀏覽器端,不存在於安卓/ios/Node.js/python/ java等其它環境
2. ✕ 跨域就是請求發不出去了
✔ 跨域請求能發出去,服務端能收到請求並正常返回結果,只是結果被瀏覽器攔截了
之所以會跨域,是因為受到了同源策略的限制,同源策略要求源相同才能正常進行通訊,即協議、域名、埠號都完全一致。
如下圖所示:
這三個源分別由於域名、協議和埠號不一致,導致會受到同源策略的限制。
同源策略具體限制些什麼呢?
1. 不能向工作在不同源的的服務請求資料(client to server)
這裡有個問題之前也困擾了我很久,就是為什麼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,所以要規避跨站請求偽造攻擊的風險,特別是涉及到錢的那種請求。