摘要:同源策略及跨域訪問同源策略同源策略約束了兩個(gè)域之間資源的加載方式,是一個(gè)很重要的安全機(jī)制用來隔離那些有潛在安全隱患的文檔。
同源策略及跨域訪問 同源策略
同源策略(Same-origin policy)約束了兩個(gè)域之間資源的加載方式,是一個(gè)很重要的安全機(jī)制用來隔離那些有潛在安全隱患的文檔。
何為源(orgin)一個(gè)源由一個(gè)URL的協(xié)議(protocol)、主機(jī)(host)和端口(port)進(jìn)行定義。如果兩個(gè)頁面擁有相同的協(xié)議、主機(jī)和端口一致的話,我們就可以稱它們?yōu)橥?。下面的表格比較了不同的URL跟http://store.company.com/dir/page.html這個(gè)URL的同源情況:
URL | 是否同源 | 理由 |
---|---|---|
http://store.company.com/dir2... | 是 | |
http://store.company.com/dir/... | 是 | |
https://store.company.com/sec... | 否 | 協(xié)議不同 |
http://store.company.com:81/d... | 否 | 端口不同 |
http://news.company.com/dir/o... | 否 | 主機(jī)不同 |
同源策略控制了兩個(gè)源之間的交互,例如你使用XMLHttpRequest發(fā)起一個(gè)請求,或者使用元素加載一張圖片,就會(huì)產(chǎn)生兩個(gè)源之間的交互。而當(dāng)兩個(gè)源不相同時(shí),有些交互會(huì)被允許,而有些則不被允許。而不允許的情況,就是我們常說的跨域訪問問題。那什么情況下不同源的交互會(huì)被允許,什么情況下又不被允許呢?大致可以分為如下的情況:
鏈接、跳轉(zhuǎn)和表單提交這些在跨域的情況下都是被允許的。例如調(diào)用支付寶接口進(jìn)行支付就是典型的跨域表單提交的場景,這種跨域的調(diào)用是被允許的。但是這個(gè)很容易被利用來進(jìn)行CSRF攻擊,所以我們的表單提交需要做好這方面的防護(hù)。
跨域的資源內(nèi)嵌是被允許的。下面是一些資源內(nèi)容的例子:
使用加載Javascript。只有同源的腳本在語法錯(cuò)誤時(shí)會(huì)顯示錯(cuò)誤信息。
使用加載CSS??缭吹腃SS文件要求使用正確的Content-Type 響應(yīng)頭。
使用加載圖片。
使用和加載媒體文件。
使用 、和加載插件。
使用 @font-face加載字體。有些瀏覽器允許加載跨域的字體,有些則不允許。
使用 和加載任何東西。
跨域文檔間使用Javascript腳本進(jìn)行交互,API的訪問有限制。例如使用ifame嵌入的頁面或者使用window.open打開的頁面,如果跟父頁面不同源,則想通過Javascript去操作父頁面的DOM,是不允許的(反過來亦然)。
舉個(gè)例子,假設(shè)有這么一個(gè)頁面http://www.example.com/index.html:
.....
然后在http://sub.example.com/iframe.html頁面對父頁面的背景色進(jìn)行修改:
......
由于兩個(gè)頁面不同源,所以子頁面對父頁面的操作被禁止,例如在Firefox上你會(huì)看到以下的報(bào)錯(cuò):
Error: Permission denied to access property "document"
不同源之間的XMLHttpRequest調(diào)用(也就是我們常說ajax調(diào)用)是不被允許的,這個(gè)也是我們最常遇到的跨域訪問場景。例如我們在http://example.com/index.html頁面進(jìn)行如下的ajax調(diào)用:
var xhr = new XMLHttpRequest(); var url = "http://otherexample.com/api/get-data"; xhr.open("GET", url, true); xhr.onreadystatechange = handler; xhr.send();
在Firefox下會(huì)報(bào)如下錯(cuò)誤:
已攔截跨源請求:同源策略禁止讀取位于 http://otherexample.com/api/get-data 的遠(yuǎn)程資源。(原因:CORS 頭缺少 "Access-Control-Allow-Origin")。跨域訪問的解決方案
一個(gè)頁面的源是可以修改的,修改的方法很簡單,就是通過Javascript腳本設(shè)置document.domain的值。舉個(gè)例子,我們在頁面http://store.company.com/dir/other.html執(zhí)行下面的代碼:
document.domain = "company.com";
那么這個(gè)頁面的域?qū)?huì)由store.company.com變成company.com,后面在判斷是否同源的時(shí)候,主機(jī)將會(huì)使用company.com這個(gè)值,而不是store.company.com。
那是不是修改域后就能跟同域的頁面進(jìn)行交互呢。答案是否定的。例如,如果你在頁面中通過iframe嵌入http://company.com/dir/page.html這個(gè)頁面,然后通過javascript去跟這個(gè)頁面交互,你會(huì)發(fā)現(xiàn)瀏覽器會(huì)報(bào)錯(cuò),就跟我們之前那個(gè)例子一樣。按照上面的源的定義,這時(shí)候兩個(gè)頁面應(yīng)該是同源的,為什么呢?因?yàn)樾薷?b>document.domain會(huì)導(dǎo)致端口號(hào)被設(shè)為null。所以另外一個(gè)頁面也需要把document.domain修改為相同的值,這樣兩個(gè)頁面的主機(jī)和端口就一致了,可以進(jìn)行互相的訪問了。
既然源可以修改,那么是不是就解決了我們的跨域問題呢?明顯沒這么簡單。首先,這種方法是有很大限制條件的,document.domain這個(gè)值只能修改為這個(gè)頁面的當(dāng)前域或者當(dāng)前域的超級(jí)域。所以,這個(gè)方法只能解決同一超級(jí)域下的頁面跨域問題。其次,它的使用場景也很有限,因?yàn)樗枰撁鎴?zhí)行Javascript腳本,所以也就是說一般只能應(yīng)用于頁面跟頁面的交互,例如訪問ifame頁面或者window.open打開的頁面等等。所以如果你想用來解決ajax之類的跨域調(diào)用,這個(gè)方法就無能為力了。
使用代理也是解決跨域訪問的一個(gè)方法。上面修改document.domain的方法只能用來訪問子域名的頁面,無法訪問不同域的頁面,而使用代理則沒有這個(gè)問題。
例如我們有一個(gè)頁面http://example.com/,需要訪問http://otherexample.com/這個(gè)頁面,我們不直接對這個(gè)頁面進(jìn)行訪問,而是通過請求另外一個(gè)同源的頁面,這個(gè)頁面在后端通過代理服務(wù)器把請求轉(zhuǎn)發(fā)到http://otherexample.com/,獲取數(shù)據(jù)并返回給客戶端。
另外,這個(gè)方法同樣可以用于解決ajax的跨域訪問問題。
JSONP也被經(jīng)常用來解決ajax的跨域調(diào)用問題。JSONP請求并不是通過XMLHttpRequest發(fā)起,而是使用
然后接口獲取到callback函數(shù)名后,把原來返回的數(shù)據(jù)作為函數(shù)的參數(shù),最終返回如下的Javascript:
myFunction({"id": "123", "name": "Captain Jack Sparrow"});
然后myFunction就會(huì)執(zhí)行,達(dá)到了調(diào)用的目的。
這個(gè)方法在大多數(shù)情況下都很有用,但是它也有它的局限。一是它需要后端的配合,因?yàn)楹蠖说慕涌谛枰鶕?jù)約定的參數(shù)獲取回調(diào)函數(shù)名,然后跟返回?cái)?shù)據(jù)進(jìn)行拼接,最后進(jìn)行響應(yīng)。二是它只能進(jìn)行異步的調(diào)用,因?yàn)樗脑硎峭ㄟ^動(dòng)態(tài)生成