瀏覽器同源策略及 Ajax 跨域解決方案

淘淘笙悅發表於2018-07-13

因為在開發過程中會經常遇到因為瀏覽器同源策略而導致的跨域問題,而多數開發者對瀏覽器同源策略和跨域問題並沒有很清晰的認識,所以打算在這篇文章中說下瀏覽器同源策略和我們最經常會遇到的 Ajax 跨域問題及其解決方案。

對於源的定義,MDN 中是這麼解釋的:如果兩個頁面的協議、域名和埠都相同,則兩個頁面具有相同的源。

從定義我們可以知道,關注兩個頁面是否同源,只要比較兩個頁面的協議域名即可。

舉個例子,假設有以下頁面,比較 A 頁面與其它頁面是否同源~

A:http://xys.ttsy/a.html 
B:http://xys.ttsy/b.html 
C:https://xys.ttsy/c.html 
D:http://d.xys.ttsy/d.html 
E:http://xys.ttsy:8081/e.html 
複製程式碼

根據定義,可以知道 A 和 B 同源,而 A 和 C、D、E 不同源。A、B 頁面同源是因為其協議(都是 http)、域名(都是 xys.ttsy)和埠(都是 80)都相同;而 A 與 C、D、E 不同源,是因為 A 和 C 不同協議(http 和 https),A 和 D 不同域名(xys.ttsy 和 d.xys.ttsy),A 和 E 不同埠(80 和 8081) 。

在瀏覽器中,一個最核心也最基本的安全功能便是同源策略。

同源策略是指瀏覽器中一個源的指令碼只能訪問同源的另一個指令碼的策略。

也就是說,在瀏覽器中的指令碼如果要訪問其它指令碼的話,那麼兩個指令碼必須是同源的,否則會受到瀏覽器同源策略的限制。如果兩個指令碼非同源,會有三個行為受到限制

  • DOM 無法獲得;
  • Cookie、LocalStorage 和 IndexDB 無法共享;
  • Ajax 請求限制;

DOM 無法獲得

DOM 無法獲得的限制最常見的是在 iframe 視窗與父視窗之間,如果父視窗與其 iframe 視窗的指令碼是不同源的,則它們互相無法獲取對方的 DOM 元素。

如下父視窗與 iframe 不同源,父視窗中無法獲取 iframe 視窗中指令碼的 DOM 元素

<iframe id="myIframe" src="http://www.taobao.com" width="500" height="500" ></iframe>
<script type="text/javascript">
    var myIframe = document.getElementById('myIframe').contentWindow.document;
    console.log(myIframe)  // 能獲取到 document 物件,但裡面沒有有效資料
    console.log(myIframe.body)  // 空的 body
</script>
複製程式碼

上述程式碼列印出來的效果如下所示

無效的 document 和 body

同理,在 iframe 視窗指令碼中也無法獲取父視窗指令碼的 DOM 元素

console.log(self.parent.document)
console.log(self.top.document)
console.log(self.parent.document.body)
console.log(self.top.document.body)
複製程式碼

那麼,有沒有什麼方式能夠規避此類同源策略的限制呢?答案是有的,只是這其中也是有條件的。

當父視窗與 iframe 視窗一級域名相同而二級域名不同的時候(或者說有共同的一級域名更為準確一點),則可以通過設定 document.domain 屬性來規避此類同源策略的限制。

只要分別在兩個視窗中對應的指令碼檔案中設定如下程式碼即可

document.domain='ttsy.com';  // 設定同一個一級域名
複製程式碼

Cookie、LocalStorage 和 IndexDB 無法共享

若兩個指令碼不同源,則 Cookie、LocalStorage 和 IndexDB 的內容無法共享。

對於 Cookie 來說,有兩種方式可以規避此類同源策略的限制。

若是一級域名相同而二級域名不同的情況(或者說有共同的一級域名更為準確一點),則可以通過設定 document.domain 屬性來規避此類同源策略的限制。

只要兩個指令碼檔案設定相同的 document.domain 值,即可共享 Cookie 。

document.domain='ttsy';  // 設定相同的值
複製程式碼

第二種方式則是伺服器端程式碼在設定 Cookie 的時候,將 domian 屬性設定為一級域名,那麼該一級域名下的子域名同樣可以共享 Cookie 。

而 LocalStorage 和 IndexDB 則無法通過上述方法來規避同源策略。

而對於完全不同源的頁面來說,還可以通過 window.name、window.postMessage 等方式來實現通訊,本篇不繼續贅述,有興趣的童鞋可以查閱相關資料瞭解。

Ajax 請求限制

在一個指令碼中,如果通過 Ajax 請求另一個非同源的指令碼,則會報錯。報錯資訊經常會類似下面醬紫

XMLHttpRequest cannot load xxx  . No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access.
複製程式碼

這即是我們經常提到的 Ajax 跨域問題,這是由於瀏覽器的同源策略引起的,Ajax 無法請求非同源的資源。

對於 Ajax 跨域解決方案,通常來說有如下三種:

  • JSONP
  • CORS
  • 代理伺服器

下面就詳細描述上述三種 Ajax 跨域解決方案。

JSONP

JSONP 是解決跨域很常用的一種方法了,其原理是通過 script 標籤向伺服器端發起請求不受瀏覽器同源策略的限制。而伺服器端在接受到請求後,可以返回一個指定名字的函式,該函式的引數是需要返回的資料。

舉個例子吶~

var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = 'http://ttsy.com/getName?callback=fn';
document.body.appendChild(script);
    
function fn(data) {
    console.log('name: ' + data.name);
};
複製程式碼

上述程式碼通過建立一個 script 標籤,將其 src 屬性設定為執行請求的 url 並將其插入到頁面中,向伺服器發起一個請求。上述程式碼中請求的 url 為 http://ttsy.com/getName?callback=fn ,指定了回撥函式名為 fn 。

而服務端在收到該請求後返回一個指定名字的函式,該函式的引數是需要返回的資料。服務端返回資料如下

 fn({
        "name": "ttsy"
    });
複製程式碼

由於通過 script 標籤請求到的資料會直接執行,所以上述服務端返回資料後會直接執行 fn 函式,所以在上述程式碼中 fn 函式將會輸出 name:ttsy 。

基於 JSONP 的實現原理,其只能通過 get 請求來獲得資料,不能進行較為複雜的 post 請求,所以在更多的情況下,我們會採用下面的 CORS 來解決 Ajax 的跨域問題。

CORS

CORS(Cross-origin resource sharing),全稱是跨域資源共享。它允許 Web 應用伺服器進行跨域訪問控制,從而使跨域資料傳輸得以安全進行。是 Ajax 跨域問題的根本解決方案。

CORS 的實現需要瀏覽器與伺服器共同支援。

目前來說,基本所有的瀏覽器都支援 CORS 功能,當瀏覽器發現 Ajax 跨域請求時,會在 HTTP 請求頭新增一些附加的頭資訊,有時會多出一次 HTTP 請求,這些都是由瀏覽器自動完成的。

而在伺服器中要實現了 CORS 功能,則需要設定 Access-Control-Allow-Origin 屬性。

Access-Control-Allow-Origin: *
複製程式碼

代理伺服器

代理伺服器是指通過設定一個同源的代理伺服器,然後將我們的 Ajax 請求傳送到我們的代理伺服器上,再通過代理伺服器去向實際的伺服器去請求資料,最後通過代理伺服器返回給瀏覽器。

通過設定代理伺服器來規避瀏覽器同源策略的限制,其原理是伺服器之間的資源請求並沒有同源策略的限制。但是由於操作起來並不方便,所以在實際開發中並不會經常用這種方法來解決問題。

覺得還不錯的小夥伴,可以關注一波公眾號哦。

瀏覽器同源策略及 Ajax 跨域解決方案

相關文章