什么是跨域
瀏覽器出于安全方面的考慮,只允許客戶端與本域(同協(xié)議、同域名、同端口,三者缺一不可)下的接口交互。不同源的客戶端腳本在沒有明確授權(quán)的情況下,不能讀寫對方的資源,這被稱為同源策略。
同協(xié)議:如都是http或者https
同域名:如都是http://xxx.com/a和http://xxx.com/b
同端口:如都是8080端口
而有時候,我們不得不在一個客戶端下訪問不同域中的資源,于是需要用到一些方法來避開瀏覽器的同源策略,這些方法被稱為跨域。
實現(xiàn)跨域有如下幾種方法:
1. JSONP
JSONP(JSON with Padding)是數(shù)據(jù)格式JSON的一種使用模式,可以使網(wǎng)頁實現(xiàn)跨域請求。其原理主要利用了 HTML的script標簽。由于script是采用開放策略,通過設置src引入不同域下的資源,所以可以通過script實現(xiàn)跨域,該方法需要后端支持。jsonp跨域的實現(xiàn)步驟如下:
- 首先客戶端定義一個數(shù)據(jù)處理函數(shù)
fun,用于處理后端服務器返回的數(shù)據(jù)。 - 創(chuàng)建一個
script標簽,并將script的src設置為后端接口,并在最后加一個參數(shù)callback=fun。 - 服務端在收到由
script標簽發(fā)出的請求后,會解析請求參數(shù),獲取callback對應的fun的字符串,然后將要返回的數(shù)據(jù)data與fun拼接為一個'fun(data)'的字符串,返回客戶端。 - 客戶端拿到響應后會放到
script標簽里執(zhí)行,此時會調(diào)用fun函數(shù),參數(shù)為data。
下面來做個演示,首先為演示方便,將系統(tǒng)的hosts做如下修改:
127.0.0.1 example.a.com
127.0.0.1 example.b.com
服務器端(用server-mock啟動,保存圖片的url地址數(shù)據(jù)):

客戶端(展示隨機圖片):

請求結(jié)果:


以上例子最終實現(xiàn)了由example.a.com到example.b.com的跨域。應注意的是,因為<script>只能發(fā)送GET請求,所以jsonp只能實現(xiàn)GET請求的跨域。如果希望能實現(xiàn)其他請求的跨域,就可以用接下來介紹的一種方法——CORS。
2.CORS
CORS(全稱為:Cross-Origin Resouce Sharing)跨域資源共享,是一種通過ajax跨域請求資源的方法。瀏覽器將CORS請求分為兩大類,簡單請求(simple request)和非簡單請求(not-so-simple request,瀏覽器對這兩種請求的處理方式不一樣。如果請求滿足以下兩個條件,則為簡單請求。
- 請求方式為HEAD,GET,POST三者中第一個。
- HTTP頭部信息不超過以下幾種字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三個值application/x-www-form-urlencoded、multipart/form-data、text/plain。
簡單請求的實現(xiàn)方式即當用XMLHttpRequest發(fā)請求時,瀏覽器如果發(fā)現(xiàn)該請求不符合同源策略,會給該請求加上一個請求頭origin,origin用來說明本次請求來自哪個源(協(xié)議+域名+端口)。如果origin指定的源不在后臺允許范圍內(nèi),后臺會返回一個正常的HTTP響應,然后瀏覽器會發(fā)現(xiàn)該響應頭部信息不包含Access-Control-Allow-Origin字段,然后拋出一個錯誤,該錯誤被XMLHttpRequest的onerror函數(shù)捕獲,響應被駁回,但因為該錯誤無法通過狀態(tài)碼識別,所以HTTP回應的狀態(tài)碼還是200。如果origin在后臺允許范圍內(nèi),則服務器返回的響應,會包含Access-Control-Allow-Origin:Origin(指定的源)信息,瀏覽器此時不會拋錯,響應能正常處理。
非簡單請求是是請求方法為PUT或DELETE,又或者Content-Type為application/json的對服務器有特殊要求的請求。非簡單請求的CORS請求,會在正式通信前增加一次HTTP查詢,稱為預檢(preflight),詢問服務器當前網(wǎng)頁所在域名是否在服務器的許可名單中,如果在,則發(fā)出正式的XMLHttpRequest,之后就與簡單請求一樣,不在則報錯。
依舊用上面的例子。
服務器端(設置一個響應頭,里面包含了允許請求的域名信息)

客戶端(用ajax發(fā)請求)

最終實現(xiàn)的效果與第一個jsonp的例子一樣。
3.降域
還有一種方式,就是通過降域來實現(xiàn)跨域。即通過設置document.domain的方式,將兩個域名的domain設置為一個,如對于a.example.com和b.example.com,可以通過js設置document.domain = "example.com",實現(xiàn)跨域。
做個演示,假設在http://a.example.com:8080下有一個a.html文件,其中a.html中有一個iframe,它的src為http://b.example.com:8080/b.html。


正常情況下,由于a.html,b.html不同源,他們之間無法正常通信,但在設置
document.domain = "example.com"后,兩邊可以互相通信。最終效果如下:

用降域方法實現(xiàn)跨域操作簡單,但是有一些缺點。比如域名只能往下設置,不能回去,比如從example.com回到a.example.com。同時如果一個子域名被攻擊,多個被降域的域名都會被連帶攻擊,有很大的安全風險。
4.postMessage
postMessage是一個web API,可以實現(xiàn)跨域通信。window.postMessage()被調(diào)用時,會在所有頁面腳本執(zhí)行完畢后,向目標窗口派發(fā)一個MessageEvent消息。語法如下:
otherWindow.postMessage(message, targetOrigin, [transfer]);
-
otherWindow表示目標窗口的一個引用,比如iframe的contentWindow屬性、執(zhí)行window.open返回的窗口對象、或者是命名過或通過數(shù)值索引的window.frames。 -
message表示需要發(fā)送到目標窗口的數(shù)據(jù)。 -
targetOrigin決定了哪些窗口可以接收消息,其值可以是字符串"*"(表示無限制)或者一個URI。 -
transfer是一串和message同時傳遞的Transferable 對象,這些對象的所有權(quán)將被轉(zhuǎn)移給消息的接收方,而發(fā)送一方將不再保有所有權(quán)。
MessageEvent具有如下屬性:
-
message屬性表示該message的類型。 - data屬性為
window.postMessage的第一個參數(shù); - origin 屬性表示調(diào)用
window.postMessage()方法時調(diào)用頁面的當前狀態(tài); - source 屬性記錄調(diào)用
window.postMessage()方法的窗口信息。
用一個與上面降域類似的例子來做演示。同樣有兩個頁面a.html和b.html,a.html中的iframe的src指向b.html。


最終實現(xiàn)a.html與b.html通信效果如下:

使用postMessage方法應注意的是,如果不希望從其他網(wǎng)站接收message,那么不要為message事件添加任何監(jiān)聽器。如果確實希望接收其他網(wǎng)站的message,那么應該始終使用origin和source屬性來驗證發(fā)件人的身份,以免被惡意的網(wǎng)站攻擊。
小結(jié)
以上就是幾種常見的跨域方法,各有優(yōu)劣,且各自都有一定的安全問題,在日常應用中,需要有針對性的使用,對可能的安全風險采取相應措施。