前端跨域問題及解決方案打賞

對于絕大多數B/S系統開發者來說,"跨域"并不是什么陌生的詞匯,但也往往因為熟悉而忽略其本質。眾所周知,Web瀏覽器往往是不安全的,出于安全考慮,Netscape公司1995年在瀏覽器中引入同源策略(same-origin policy),目的是為了保證用戶信息的安全,防止惡意的網站竊取數據。目前,所有瀏覽器都實行這個政策,"跨域"問題也由此形成。

所謂同源,簡單的講,就是網址必須是同協議、同域名(主域、子域與不同子域都稱為非同域名)、同端口,兩個網址出現任一項不同則出現跨域。

受同源策略影響,出現跨域的情況,我們不能做以下三類事

1)不能操作cookie、LocalStorage(SessionStorage) 和 IndexDB
2)不能操作DOM
3)不能發送AJAX 請求

顯然,同源策略影響的不止是"惡意網站",合理的用途也受到影響。下面將介紹如何規避上面三種限制。

一、操作cookie、LocalStorage(SessionStorage) 和 IndexDB

1、cookie

同主域情況下,對于cookie的操作,經常會遇到有同事說某某cookie刪不掉等問題,實際多為domain或者path不同所致,對于此類問題,我們只需設置document.domain為根域或在設置cookie時保證域名為根域(如"poorren.com")且path為根路徑"/"即可。對于不同主域,依然是不能規避的,當然,這要排除早些年使用flash腳本惡意抓取第三方網站cookie的方式,不過flash日漸衰敗的今天,也可以理解為互聯網安全又向前邁了一步。

2、LocalStorage(SessionStorage) 和 IndexDB

使用傳統手段無法規避,但HTML5為了解決這個問題,引入了一個全新的API:跨文檔通信 API(Cross-document messaging)。每個window下面都有postMessage方法,允許跨窗口通信,不受同源策略影響。通過此API,我們可以簡單的在當前頁面(如:www.pswuul.tw)下執行

window.postMessage('data','http://user.poorren.com');

在user.poorren.com添加事件

window.addEventListener('message', function(e) {
    console.log(e.data);
},false);

在message事件中,可以獲取發送消息的窗口(e.source)、消息發向的網址(e.origin)和消息內容(e.data),需要注意的是,這里data的傳輸類似LocalStorage(SessionStorage),只能是字符串,不過這毫不影響我們的使用,任何需求只需要發送、接收兩端頁面代碼都是我們所能控制的,就可以輕松實現,具體實現就不再舉例,大家可以盡情腦補各種可能性啦。

二、操作DOM

跨域所影響的操作DOM問題即iframe內嵌網站需要進行交互導致,如父頁面操作子頁面

document.getElementById("iframe").contentWindow.document

或者子頁面操作父頁面

window.parent.document

都會出現跨域警告,這類情況下如果主窗口與iframe內網址為同根域,則可參考cookie處理方式,設置document.domain為根域,對于完全不同源的網站,有window.name、片段識別符(fragment identifier)、跨文檔通信 API(Cross-document messaging)三種方案。

1、window.name

瀏覽器窗口有window.name屬性。這個屬性的最大特點是,無論是否同源,只要在同一個窗口里,前一個網頁設置了這個屬性,后一個網頁可以讀取它,最大可以存儲2M的數據,但前提是子窗口寫入數據后載入同源頁面或者空頁面,如

var iframe = document.getElementById('iframe');
var data = '';
iframe.onload = function(){
    iframe.onload = function(){
        data = iframe.contentWindow.name;
    }
    iframe.src = 'about:blank';
};

注:about:blank,javascript: 和 data: 中的內容,繼承了載入他們的頁面的源。當然,這里兩側頁面內都需要對window.name進行監聽才能實現數據傳遞。

2、片段識別符(fragment identifier)

通俗說,其實就是通過location.hash取到的錨點信息,通過window.onhashchange可以監聽當前頁面location.hash的變化,這點和目前流行的前端路由其實是使用了同一特性,只不過是監聽背后做了不同的事。

3、跨文檔通信 API(Cross-document messaging)

前兩種為早期方案,都不完美,基本思想都是一側寫數據,另外一側監聽,繼而實現各種需求。第三種可以說是思想一致,只不過等了這么久終于有了"正規手段"來解決此類問題,具體使用第一段已經提及,這里不多贅述

三、發送AJAX

可能很多朋友第一次遇到跨域問題就是在使用AJAX過程中吧,對于AJAX跨域,有代理、JSONP、WebSocket、CORS四種方案。

1、代理

常見各種服務器語言或者服務器中間件皆可實現,不在前端討論范圍,這里一帶而過

2、JSONP

基本思想是,依托script不受同源策略限制的特性,將后端api返回內容包裝為js腳本的形式輸出(這里需要后端配合變更輸出內容),js中通過動態創建script標簽,指定src為api地址,待script加載完畢則執行動態插入script中的函數,簡單示例如下

function jsonp(src) {
    var script = document.createElement('script');
    script.setAttribute("type","text/javascript");
    script.src = src;
    document.body.appendChild(script);
}

jsonp('http://poorren.com/api?callback=callback');

function callback(data) {
    console.log(data);
};

jQuery等前端工具庫都是采用此特性實現,這里不得不提一句,很多初入前端的小朋友認為jQuery的jsonp也可以設置type為post,所以jsonp支持post,相信看到這里就不會這么想了,script標簽加載信息,始終都是get請求。

3、WebSocket

WebSocket是一種通信協議,也是HTML5中新增加的一項特性,使用ws://(非加密)和wss://(加密)作為協議前綴。該協議不實行同源政策,只要服務器支持,就可以通過它進行跨源通信。由于目前多數B/S應用并沒有大規模使用WebSocket的需求,這里一帶而過,不多做介紹。

4、CORS

CORS是一個新的W3C標準,即Cross Origin Resource Sharing(跨來源資源共享),有了CORS,代理、JSONP等方式終將成為過去式。

瀏覽器將CORS請求分成兩類:簡單請求(simple request)和復雜請求[非簡單請求](not-so-simple request)。

只要同時滿足以下兩大條件,就屬于簡單請求。

1)請求方法是HEAD、GET、POST三種方法之一
2)HTTP的頭信息不超出Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(值只能為application/x-www-form-urlencoded、multipart/form-data、text/plain其中一種)幾種字段

任何一個不滿足上述要求的請求,即被認為是復雜請求。一個復雜請求不僅有包含通信內容的請求,同時也包含預請求(preflight request)。

簡單請求:

簡單請求的發送從代碼上來看和普通的XHR沒太大區別,但是HTTP頭當中要求總是包含一個域(Origin)的信息。該域包含協議名、地址以及一個可選的端口。不過這一項實際上由瀏覽器代為發送,并不是開發者代碼可以觸及到的。

簡單請求的部分響應頭及解釋如下:

Access-Control-Allow-Origin(必含)- 不可省略,否則請求按失敗處理。該項控制數據的可見范圍,如果希望數據對任何人都可見,可以填寫"*"。

Access-Control-Allow-Credentials(可選) – 該項標志著請求當中是否包含cookies信息,只有一個可選值:true(必為小寫)。如果不包含cookies,請略去該項,而不是填寫false。這一項與XmlHttpRequest2對象當中的withCredentials屬性應保持一致,即withCredentials為true時該項也為true;withCredentials為false時,省略該項不寫。反之則導致請求失敗。

Access-Control-Expose-Headers(可選) – 該項確定XmlHttpRequest2對象當中getResponseHeader()方法所能獲得的額外信息。通常情況下,getResponseHeader()方法只能獲得Cache-Control、Content-Language、Content-Type、Expires、Last-Modified和Pragma。

上面說到,CORS請求默認不發送cookie和HTTP認證信息。如有需要,一方面要服務器同意,指定Access-Control-Allow-Credentials字段。

Access-Control-Allow-Credentials: true

另一方面,必須在AJAX請求中打開withCredentials屬性。

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否則,即使服務器同意發送cookie,瀏覽器也不會發送。或者,服務器要求設置cookie,瀏覽器也不會處理。但如果省略withCredentials設置,有的瀏覽器還是會一起發送cookie。所以可以顯式關閉withCredentials。

xhr.withCredentials = false;

需要注意的是,如果要發送cookie,Access-Control-Allow-Origin就不能設為星號,必須指定明確的、與請求網頁一致的域名。同時,cookie依然遵循同源政策,只有用服務器域名設置的cookie才會上傳,其他域名的cookie并不會上傳,且(跨源)原網頁代碼中的document.cookie也無法讀取服務器域名下的cookie。

復雜請求:

復雜請求表面上看起來和簡單請求使用上差不多,但實際上瀏覽器發送了不止一個請求。其中最先發送的是一種"預請求",此時作為服務端,也需要返回"預回應"作為響應。預請求實際上是對服務端的一種權限請求,只有當預請求成功返回,實際請求才開始執行。預請求以OPTIONS形式發送,請求頭在簡單請求的基礎上增加了兩項CORS特有的內容:

1)Access-Control-Request-Method – 該項內容是實際請求的種類,可以是GET、POST之類的簡單請求,也可以是PUT、DELETE等等。
2)Access-Control-Request-Headers – 該項是一個以逗號分隔的列表,當中是復雜請求所使用的頭部。

顯而易見,這個預請求實際上就是在為之后的實際請求發送一個權限請求,在預回應返回的內容當中,服務端應當對這兩項進行回復,以讓瀏覽器確定請求是否能夠成功完成。如上所述,假如請求時傳入

Access-Control-Request-Method: PUT
Access-Control-Request-Headers: token

響應頭

Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: token

則預請求成功,否則失敗

復雜請求的部分響應頭及解釋如下:

Access-Control-Allow-Origin(必含) – 和簡單請求一樣的,必須包含一個域,但不能為"*"。

Access-Control-Allow-Methods(必含) – 這是對預請求當中Access-Control-Request-Method的回復,這一回復將是一個以逗號分隔的列表。盡管客戶端或許只請求某一方法,但服務端仍然可以返回所有允許的方法,以便客戶端將其緩存。

Access-Control-Allow-Headers(當預請求中包含Access-Control-Request-Headers時必須包含) – 這是對預請求當中Access-Control-Request-Headers的回復,和上面一樣是以逗號分隔的列表,可以返回所有支持的頭部。這里在實際使用中有遇到,所有支持的頭部一時可能不能完全寫出來,而又不想在這一層做過多的判斷,沒關系,事實上通過request的header可以直接取到Access-Control-Request-Headers,直接把對應的value設置到Access-Control-Allow-Headers即可。

Access-Control-Allow-Credentials(可選) – 和簡單請求當中作用相同。

Access-Control-Max-Age(可選) – 以秒為單位的緩存時間。預請求的的發送并非免費午餐,允許時應當盡可能合理的緩存,不過這里有網友實測設置超過15分鐘的緩存,時段內依然會再次預請求,猜測可能是瀏覽器bug等因素,這里沒做過多追溯。

一旦預回應如期而至,所請求的權限也都已滿足,則實際請求開始發送。

通http://caniuse.com/#search=cors得知,目前大部分Modern瀏覽器已經支持完整的CORS,但IE直到IE11才完美支持,所以對于PC網站,如果考慮低版本IE,還是建議優先采用其他解決方案,如果僅僅是移動端網站,大可放心使用。

注:之前整理過關于CORS的文章《CORS跨域請求[簡單請求與復雜請求]》,最近應公司征集前端相關的文章的要求把其他跨域解決方案大概整理一下,因時間倉促,網上收集資料參考整理后輸出此文,也順便在博客發一下。文章資源整理自網絡,如有版權問題請聯系,我會及時移除。

前端跨域問題及解決方案
文章《前端跨域問題及解決方案》二維碼
  • 微信打賞
  • 支付寶打賞

已有4條評論

  1. 游客 123

    學習了 原來是這樣解決!

    2017-08-11 17:45 回復
  2. 加氣塊設備

    受教了 謝謝

    2017-07-28 08:35 回復
  3. 任務易

    好像在哪里看到過里面的內容

    2017-07-26 15:19 回復
  4. 自由職業

    學習了~!!

    2017-06-27 10:31 回復

(必填)

(必填)

(可選)

黑龙江22选5开奖