跨域
最近在回顧一些知識,歸納一下以前的筆記再結合各個資料說一下我對跨域和跨域問題的解決方法。
產生跨域安全問題不是後臺伺服器不允許前臺呼叫,
其本質是瀏覽器的同源策略(Same-origin policy)造成的,它是瀏覽器最基本和最核心的安全機制,同源是指URI scheme
、host name
、port number
相同,借用一下網上的栗子:
http://www.bear.cn/index.html 呼叫 http://www.bear.cn/server.php 非跨域
http://www.bear.cn/index.html 呼叫 http://www.jasmine .cn/server.php 跨域,主域不同
http://gogo.bear.cn/index.html 呼叫 http://ge.jasmine.cn/server.php 跨域,子域名不同
http://www.bear.cn:2018/index.html 呼叫 http://www.bear.cn/server.php 跨域,埠不同
https://www.bear.cn/index.html 呼叫 http://www.bear.cn/server.php 跨域,協議不同
複製程式碼
如果非同源,將會受到如下限制:
- Cookie、LocalStorage 和 IndexDB 無法讀取。
- DOM 無法獲得。
- AJAX 請求不能傳送。
瀏覽器發現前臺程式碼發出了一個非本域的請求,出於安全的考慮,瀏覽器會做一些校驗,如果校驗不通過,就無法完成這個請求,丟擲請求跨域的錯誤
Jsonp
JSONP是JSON with padding(填充式JSON或引數式JSON)的簡寫,是應用JSON的一種辦法,JSONP看起來和JSON差不多,只不過是被包含在函式呼叫中的JSON,就像這樣:
callback({"name": "Nicholas Bear"})
複製程式碼
JSONP由兩部分組成:回撥函式和資料。回撥函式是當瀏覽器接收到響應時呼叫的函式,回電函式名一般在請求中指定,資料就是回撥函式的引數。如下就是典型的JSONP請求:
http://somewhere-else/json/?callback=handleResponse
複製程式碼
這裡指定的回撥函式就是handleResponse()
JSONP實現原理是通過JS指令碼動態生成一個script元素,為其src屬性指定一個跨域URL,這裡的script元素和img、link元素類似,都有能力不受限制地從其他域載入資源。它並不是官方的協議,而是一種hack手段,看一個簡單的栗子:
function handleResponse(res) {
alert("got message", res);
}
var script = document.createElement("script"),
body = document.body;
script.src = "http://somewhere-else/json/?callback=handleResponse";
body.insertBefore(script, body.firstChild);
複製程式碼
JSONP實現跨域訪問非常方便,簡單易用,但是也有不足的地方:
首先,從它的實現方式可以看出來,它是發起一個資源獲取請求,是GET
型別的,在日常開發中常用的請求型別還有POST
,PUT
,DELETE
,而JSONP只能發起GET
請求,是它的一大短板。
其次,JSONP是從其他域中載入程式碼並執行,如果其他域不安全,很有可能會在執行的程式碼中夾雜一些惡意程式碼,所以在使用JSONP時一定要保證被請求方它安全可靠。
另外,JSON和JSONP還有一個區別需要特別注意,JSONP請求返回來的不是JSON資料,而是一個JavaScript指令碼,為了實現JSONP跨域,需要後臺伺服器配合。
最後,由於它的請求型別並不是XHR,就缺少了一些事件處理程式,要追蹤JSONP請求是否失敗並不容易,或者為JSONP請求增加定時器,超時就視為請求失敗,接下來就再次傳送請求或者做其他事情,但是每個使用者的網路狀況並不能保證,這樣做也不是萬全之策。
CORS
CORS(Cross-origin resource sharing)跨域源資源共享,是W3C的一個工作草案, 定義了在跨域訪問時,瀏覽器與伺服器的溝通方式,具體實現為,使用自定義的HTTP頭部讓瀏覽器與伺服器進行溝通,從而決定跨域請求或響應時應該成功,還是應該失敗。
比如說發起一個GET
跨域請求,Content-type是text/plain,在傳送跨域請求前,瀏覽器會為http頭部加上一個額外的Origin頭部,其中包含了頁面的源資訊(協議、域名和埠號),這個額外的Origin決定了伺服器是否響應該請求。一個Origin頭部例項:
Origin: https://www.somewhere-else.net
複製程式碼
如果伺服器認可該請求就會在響應頭加上Access-Control-Allow-Origin標誌欄位,值可以是與請求頭帶來的Origin相同,如果該伺服器上的是公共資源,值就是“*”。
Access-Control-Allow-Origin: https://www.somewhere-else.net
複製程式碼
如果響應頭中沒有這個這個欄位,說明伺服器拒絕了這次跨域請求,會丟擲一個錯誤,但是並不能被xhr的onerror
事件捕獲。預設情況下跨域請求都是不帶憑證的(cookie,HTTP認證及服務端SSL證明等),通過修改xhr物件的withCredentials
(IE10以前的版本不支援該屬性)設定為true,可以指定某個請求攜帶憑證。如果伺服器允許跨域請求攜帶憑證響應頭部會有標示。
Access-Control-Allow-Credentials: true
複製程式碼
如果傳送的是帶憑證的請求,響應頭裡卻沒有這個欄位,那麼瀏覽器就不會吧響應交給JS,意思是xhr獲取到的responseText
為空,status
為0,這個時候onerror
可以捕獲到該錯誤.
XHR物件在跨域時也是有限制的:
- 不能使用
setRequestHeader()
來設定頭部 - 預設情況下無法傳送cookie
- 呼叫
getAllResponseHeaders()
方法總會返回空字串
CORS的實現:
var xhr = new XMLHttpRequest();
xhr.onreadystateChange = function() {
if(xhr.readyState === 4) {
if(xhr.status >= 200 && xhr.status <= 300 || xhr.status === 304) {
alert(xhr.responseText);
} else {
alert("error ", xhr.status);
}
}
}
xhr.open("get", "http://www.somewhere-else.com/page", true);
xhr.send(null);
複製程式碼
傳送CORS請求和傳送普通的xhr物件差別不大, 只需要在地址處寫絕對地址即可.跨域所需要做的工作就交給瀏覽器,對於使用者來說是透明.
IE瀏覽器是用XDR(XDomainRequest)來實現CORS的,它和XHR相似,但是能提供能安全可靠的跨域通訊:
- cookie不會隨請求傳送,也不會隨響應返回
- 只能設定請求頭部資訊中的Content-Type欄位
- 不能訪問響應頭部資訊
- 只支援
GET
和POST
請求
XDR物件和xhr的使用方法型別,也是創造一個XDomainRequest的例項,呼叫open()方法,再呼叫send()方法,但是與xhr物件的open()不同,XDR物件的open()方法只接受兩個引數:請求的型別和URL,XDR傳送的請求都是非同步執行的。而且XDR物件無法訪問status屬性,所以在使用XDR時一定得通過onerror
事件處理程式來捕獲錯誤.
簡單請求
跨域請求在傳送前,瀏覽器會檢查這個請求是不是簡單請求,簡單請求滿足下面兩個條件:
- 請求方式為
HEAD
,POST
,GET
- HTTP頭部資訊包括但不超過以下欄位
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type(application/x-www-form-urlencode,multipart/form-data,text/plain)
如果滿足這些條件,瀏覽器就會在請求頭部增加額外的Origin欄位後傳送跨域請求。
響應頭一般包含這些欄位:
- Access-Control-Allow-Origin,如果瀏覽器校驗通過,這個欄位顯示的是請求頭的Origin值或者*
- Access-Control-Allow-Credential,值為布林型,表示請求頭是否可以攜帶cookie
- Access-Control-Expose-Headers。擴充的頭部資訊,瀏覽器將CORS響應交給JS後,XMLHttpRequest物件的getResponseHeader()方法只能拿到6個基本欄位:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他欄位,就必須在Access-Control-Expose-Headers裡面指定。
注意,如果你想在請求中攜帶憑證,上面已經說過了,必須將xhr的withCrediential
屬性設定為true,但有時會報錯,錯誤資訊如下圖:
Access-Control-Allow-Origin
必須和請求頭中的Origin一致,而不能是“*”,解決方法很簡單,修改一下後端程式碼就可以了。
非簡單請求
CORS通過一種叫做Preflighted Requestes
預請求的透明伺服器驗證機制支援開發人員使用自定義的頭部,GET和POST之外的方法,以及不同型別的主題內容。也就是說想要傳送這種非簡單的跨域請求以前會先傳送一個詢問請求(攜帶非簡單請求部分資訊)來詢問伺服器是否同意這次非簡單請求,這種詢問請求使用OPTIONS方法,傳送以下頭部:
- Origin:和簡單請求相同
- Access-Control-Request-Method:請求自身使用的方法
- Access-Control-Request-Headers:這是一個可選頭部欄位,多個頭部以逗號分開。
傳送這個請求以後,伺服器可以決定是否允許這種型別的請求。伺服器可以通過在響應頭中攜帶以下頭部與瀏覽器溝通:
- Access-Control-Allow-Origin:和簡單請求相同
- Access-Control-Allow-Methods:允許的方法
- Access-Control-Allow-Headers: 允許的頭部
- Access-Control-Max-Age: 預請求的有效期或者快取存活時間(秒)
比如說我現在傳送了一個自定義頭部欄位f-headers1
和f-headers2
,方法為post的非簡單請求,那麼首先傳送的預請求頭部會包含以下資訊:
Origin: http://www.yourhostname.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: f-headers1, f-headers2
複製程式碼
如果伺服器允許這樣的非簡單請求的跨域訪問,返回的響應頭會包含這些欄位:
Access-Control-Allow-Origin: http://www.yourhostname.com
Access-Control-Allow-Method: POST,GET,PUT,DELETE
Access-Control-Allow-Headers: f-headers1, f-headers2
Access-Control-Max-Age: 3600
複製程式碼
預請求結束後,結果將按照響應中指定的時間快取起來,下次再傳送這樣的非簡單請求之前就不會再傳送詢問請求.
Cookie
上述幾條都是解決跨域請求資源,但是如果想要獲取非同源的cookie,LocalStorage或IndexDB怎麼辦。cookie是伺服器在瀏覽器上寫下的一小段認證資訊,大小一般是4k,根據瀏覽器的不同,每個域允許種下的cookie數量也不同。cookie只有在同源的域下才能共享,但是我們可以通過修改document.domain來共享cookie,如下所示
// a.abc.com
document.domain = "abc.com";
document.cookie = "name=bingo";
// b.abc.com
document.domain = "abc.com";
console.log(document.cookie); // "name=bingo"
複製程式碼
但是這種方法前提是這兩個網頁一級域名相同,一級域名或者叫根域名相同是什麼意思呢,比如說這裡有個兩個域名www.abc.com
和www.f.abc.com
它們的一級域名都是abc.com
。二級域名就是增加了一級包括www
,比如說www.zdt.com
,netgo.ccdn.com
,www.baidu.com
等等.三級,四級域名同理.
而且這種方法只適用於cookie和iframe.無法獲取locastorage和IndexDB.
iframe
利用iframe解決跨域問題也是一種可取的辦法.光是給iframe增加src獲取其他頁面的資源是不現實,必須藉助一些特性實現hack手段.
document.domain
兩個iframe之間或者父視窗和子視窗之間。如上述例子裡通過改變相同主域的document.domain
可以跨域獲取cookie,也可以獲取對方的全域性變數。這種方法和跨域獲取cookie一樣,只適合具有相同主域的跨域訪問。實現原理為相同主域的網站設定相同的document.domain
,瀏覽器就任務它們是同源的,這種方式比較簡單,但也有安全問題,如果某一個網站被攻擊後,另一個網站就會有安全漏洞
window.name
window.name
,它具有更新了頁面的location更新後,值依然不會更變的神奇特性,這讓我們跨域訪問資訊提供了機會。在一個頁面中建立一個不同域的iframe,這個iframe的js程式碼修改它window.name
的值,然後再將它變為和父視窗同域的iframe,在父視窗中就可以通過iframe獲得修改過後的window.name
的值
location.hash
location.hash
又稱片段識別符號(Fragment Identitier),它是URL字元中#
後面的部分,比如http://www.somewhere-else.com/a.html#fragment
,這裡的片段識別符號就是fragment,URL中的片段識別符號改變並不會引起頁面重新整理.利用location.hash
實現跨域訪問資訊的原理是父視窗可以讀寫子視窗的URL,子視窗只能讀寫相同域父視窗的URL.這裡想要實現跨域,不同域的子視窗就必須藉助一個與父視窗同域的代理. 舉個例子
a.abc.com/index.html
(a)下有一個src為smg.com/index.html
(b)的iframe.
1.a頁面給b頁面傳送資料
- a修改b的src為
smg.com/index.html#data
- b頁面訪問自己的
location.hash
即可拿到資料
2.b頁面給a頁面傳送資料,b由於不能修改不同域父視窗的URL,所以b頁面需要動態建立一個和父視窗同域的iframe來做代理.
- b頁面建立一個src為
a.abc.com/proxy.html#data
的子視窗 - 這個proxy頁面通過onhashchange(相容情況)事件監聽自己href的變化,事件觸發後通過修改a頁面的hash來達到傳遞資料的功能
- a頁面訪問自己的
location.hash
即可拿到資料
postMessage
不管是iframe和location.hash、document.domain還是window.name都是屬於非官方的跨越方法,下面要介紹的就是一個官方方法---postMessage
,它是HTML5新增的一個跨文件通訊API,它實現了即使不同域也可以跨視窗直接通訊的功能,而且只要使用得當,這種方法就很安全。
呼叫物件為父視窗或者的window物件、window.open()的返回值或者是iframe的contentWindow這個屬性,這個方法接受兩個引數,第一個是要傳送的訊息,第二個引數是指定接受訊息的接收源,可以是*表示所有視窗都可以接收到訊息或者是一個url,但只有在協議,域名和埠號都相同才會接收到訊息。
新增以下程式碼即可接收
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event)
{
// For Chrome, the origin property is in the event.originalEvent
// object.
var origin = event.origin || event.originalEvent.origin;
if (origin !== "http://example.org:8080")
return;
// ...
}
複製程式碼
事件event物件有三個屬性
- data,傳送過來的資訊
- origin,傳送發視窗的origin
- source,對傳送訊息的視窗物件的引用; 您可以使用此來在具有不同origin的兩個視窗之間建立雙向通訊
參考資料
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
http://www.ruanyifeng.com/blog/2016/04/cors.html
http://blog.csdn.net/kongjiea/article/details/44201021
《JavsScript高階程式設計(第三版)》