詳解瀏覽器跨域的幾種方法

摘要:本文針對瀏覽器的跨域特性,做一下深入介紹,以便我們在進行WEB前端開發和測試時,對瀏覽器跨域特性有全面的理解和掌握。

1 前言

在WEB前端開發中,我們經常會碰到「跨域」問題,最常見的就是瀏覽器在A域名頁面發送B域名的請求時會被限制。跨域問題涉及到WEB網頁安全性問題,使用不當會造成用戶隱私泄露風險,但有時業務上又需要進行跨域請求。如何正確的使用跨域功能,既能滿足業務需求,又能夠滿足安全性要求,顯得尤為重要。

本文針對瀏覽器的跨域特性,做一下深入介紹,以便我們在進行WEB前端開發和測試時,對瀏覽器跨域特性有全面的理解和掌握。

2 背景知識介紹

2.1 同源政策

1995年,同源政策由 Netscape 公司引入瀏覽器。目前,所有瀏覽器都實行這個政策。

最初,它的含義是指,A 網頁設置的 Cookie,B 網頁不能打開,除非這兩個網頁「同源」。所謂「同源」指的是「三個相同」:

  • 協議相同
  • 域名相同
  • 埠相同

同源政策的目的,是為了保證用戶資訊的安全,防止惡意的網站竊取數據。

設想這樣一種情況:

A 網站是一家銀行,用戶登錄以後,A 網站在用戶的機器上設置了一個 Cookie,包含了一些隱私資訊(比如存款總額)。用戶離開 A 網站以後,又去訪問 B 網站,如果沒有同源限制,B 網站可以讀取 A 網站的 Cookie,那麼隱私資訊就會泄漏。更可怕的是,Cookie 往往用來保存用戶的登錄狀態,如果用戶沒有退出登錄,其他網站就可以冒充用戶,為所欲為。因為瀏覽器同時還規定,提交表單不受同源政策的限制。由此可見,同源政策是必需的,否則 Cookie 可以共享,互聯網就毫無安全可言了。

當前,如果非同源,共有三種行為受到限制:

  • Cookie、LocalStorage 和 IndexDB 無法讀取
  • DOM 無法獲得
  • AJAX 請求不能發送

2.2 為什麼要有跨域限制

Ajax 的同源策略主要是為了防止 CSRF(跨站請求偽造) 攻擊,如果沒有 AJAX 同源策略,相當危險,我們發起的每一次 HTTP 請求都會帶上請求地址對應的 cookie,那麼可以做如下攻擊:

  • 用戶登錄了自己的銀行頁面 mybank.com,mybank.com向用戶的cookie中添加用戶標識。
  • 用戶瀏覽了惡意頁面 evil.com。執行了頁面中的惡意AJAX請求程式碼。
  • evil.com向//mybank.com發起AJAX HTTP請求,請求會默認把//mybank.com對應cookie也同時發送過去。
  • 銀行頁面從發送的cookie中提取用戶標識,驗證用戶無誤,response中返回請求數據。此時數據就泄露了。
  • 而且由於Ajax在後台執行,用戶無法感知這一過程。

DOM同源策略也一樣,如果 iframe 之間可以跨域訪問,可以這樣攻擊:

  • 做一個假網站,裡面用iframe嵌套一個銀行網站 mybank.com。
  • 把iframe寬高啥的調整到頁面全部,這樣用戶進來除了域名,別的部分和銀行的網站沒有任何差別。
  • 這時如果用戶輸入帳號密碼,我們的主網站可以跨域訪問到//mybank.com的dom節點,就可以拿到用戶的輸入了,那麼就完成了一次攻擊。

所以有了跨域訪問限制之後,我們才能夠安全的上網。

3 瀏覽器跨域的解決方案

3.1 CORS標準

CORS 是一個 W3C 標準,全稱是跨域資源共享(CORSs-origin resource sharing),它允許瀏覽器向跨源伺服器,發出XMLHttpRequest請求。

其實,準確的來說,跨域機制是阻止了數據的跨域獲取,不是阻止請求發送。

CORS需要瀏覽器和伺服器同時支援。目前,所有瀏覽器都支援該功能,IE瀏覽器不能低於IE10。

//caniuse.com/#search=cors

整個CORS通訊過程,都是瀏覽器自動完成,不需要用戶參與。對於開發者來說,CORS通訊與同源的AJAX通訊沒有差別,程式碼完全一樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭資訊,有時還會多出一次附加的請求,但用戶不會有感覺。

因此,實現CORS通訊的關鍵是伺服器。只要伺服器實現了CORS介面,就可以跨域通訊。

3.2 CORS跨域判定的總體流程

如圖所示,跨域的判定流程為:

  • 網頁上的JS程式碼,從瀏覽器上發送XMLHttpRequest請求到服務端
  • 如果該請求為簡單請求,瀏覽器會直接發送實際請求到服務端,瀏覽器會根據服務端的響應,判斷該請求是否可以跨域:

        (1)如果不能跨域,瀏覽器會報錯,阻止JS程式碼進一步執行;

       (2)如果能夠跨域,則JS能正常處理響應,進行後續業務流程

  • 如果該請求為非簡單請求,瀏覽器會先發送一個預檢請求(preflight),方法為OPTIONS,然後針對伺服器的響應,做上述跟簡單請求一樣相同的判斷:

       (1)如果不能跨域,則實際請求不會發送

      (2)如果能夠跨域,則實際請求會進行發送,進行後續業務處理

值得說明的是,瀏覽器在跨域的情況下,請求都會發送出去,但是對於響應會判斷是否滿足跨域條件,如果不滿足,則報錯,阻止JS後續的執行流程,例如讀取響應數據等。也就是說,跨域機制主要是阻止數據的跨域獲取,不是阻止請求的發送。

3.3 簡單請求

實際上瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。

只要同時滿足以下條件,就屬於簡單請求,一般來說,只需要滿足前兩個即可:

  • 請求方法是如下三種方法之一:GET、POST、HEAD
  • HTTP消息頭不超過如下幾個欄位:

          Accept

         Accept-Language

        Content-Language

        Last-Event-ID

        Content-Type:只限於三個值:application/x-www-form-urlencoded、multipart/form-data、text/plain

  • 請求中的任意XMLHttpRequestUpload 對象均沒有註冊任何事件**器
  • XMLHttpRequestUpload 對象可以使用 XMLHttpRequest.upload 屬性訪問。請求中沒有使用 ReadableStream 對象

對於簡單請求,瀏覽器會直接發起CORS請求,將實際請求發給伺服器,伺服器返迴響應給瀏覽器,同時在響應頭域中攜帶CORS相關頭域,供瀏覽器進行跨域判斷。

3.4 非簡單請求

非簡單請求時指那些對伺服器有特殊要求的請求,比如請求方法是 PUT或 DELETE,或者 Content-Type 的類型是 application/json。簡而言之,不是簡單請求的HTTP請求,都是非簡單請求。

非簡單請求的 CORS 請求,會在正式通訊之前,使用 OPTIONS 方法發起一個預檢(preflight)請求到伺服器,瀏覽器先詢問伺服器,當前網頁所在的域名是否在伺服器的許可名單之中,以及可以使用哪些 HTTP方法和頭資訊欄位。只有得到肯定答覆,瀏覽器才會發出正式的 XMLHttpRequest 請求,否則就報錯。

3.5 CORS相關頭域

那麼,無論是簡單請求,還是非簡單請求,瀏覽器都會對響應頭域中的CORS相關欄位進行判斷,CORS的常見欄位有如下幾個:

3.5.1 Access-Control-Allow-Origin(必選)

涉及簡單Http請求、非簡單Http請求

含義:允許的域名,只能填 *(通配符)或者單域名

舉例:

//www.huaweicloud.com網頁,發送//portal.huaweicloud.com請求,如果伺服器響應頭域中沒有填寫Access-Control-Allow-Origin,瀏覽器會報錯:

或者取值不為//www.huaweicloud.com,瀏覽器也會報錯:

填寫為*或者//www.huaweicloud.com,則不會報錯:

3.5.2 Access-Control-Allow-Credentials(可選)

涉及簡單Http請求、非簡單Http請求

含義:表示是否允許發送Cookie,只有一個可選值:true(必為小寫)。如果不包含cookies,請略去該項,而不是填寫false。這一項與 XmlHttpRequest 對象當中的 withCredentials 屬性應保持一致,即 withCredentials 為true時該項也為true;withCredentials 為false時,省略該項不寫。反之則導致請求失敗。

舉例:

當XmlHttpRequest中設置了withCredentials為true,如果伺服器響應里沒有Access-Control-Allow-Credentials欄位,則瀏覽器會報錯:

特別的,當XmlHttpRequest中設置了withCredentials為true時,還要求Access-Control-Allow-Origin欄位不能為通配符*,其實這也好理解,因為設置了withCredentials,表示允許跨域發送Cookie,如果Origin允許為*的話,安全性就會大大降低了,很容易構造跨站攻擊:

3.5.3 Access-Control-Expose-Headers(可選)

涉及簡單Http請求、非簡單Http請求

含義:CORS請求時,XMLHttpRequest對象的getResponseHeader()方法只能拿到6個基本欄位:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他欄位,就必須在Access-Control-Expose-Headers裡面指定。

舉例:

在JS程式碼中,通過XMLHttpRequest對象來獲取響應中的wise_traceid頭域,如:

xhr. getResponseHeader(“wise_traceid”)

如果在伺服器響應中,沒有攜帶Access-Control-Expose-Headers或者Access-Control-Expose-Headers的值不包含wise_traceid,則瀏覽器會報錯,JS拿到的值也是null:

3.5.4 預檢請求preflight

根據上述分析,如果是非簡單Http請求,瀏覽器會先發送一個預檢請求,要求伺服器進行確認。預檢請求使用的方法是OPTIONS,表示這個請求是用來詢問的,頭資訊裡面,關鍵欄位是Origin,表示請求來自哪個源。

除了Origin欄位,”預檢”請求的頭資訊包括兩個特殊欄位:

  • Access-Control-Request-Method

該欄位是必須的,用來列出瀏覽器的CORS請求會用到哪些HTTP方法,如PUT:

  • Access-Control-Request-Headers

該欄位是一個逗號分隔的字元串,指定瀏覽器CORS請求會額外發送的頭資訊欄位,當使用了簡單請求那5個欄位之外的欄位,瀏覽器會在OPTIONS請求頭域中,指定 Access-Control-Request-Headers的取值,如:

如果預檢請求的響應,瀏覽器沒有校驗通過,不允許跨域,瀏覽器除了會在控制台報錯之外,後續實際請求也不會發送了。

3.5.5 Access-Control-Allow-Methods(必選)

涉及非簡單Http請求

含義:允許跨域請求的 http 方法(如POST、GET、OPTIONS、PUT、DELETE),該欄位是對於預檢請求中的Access-Control-Request-Method的回復。

備註:對於簡單請求的GET、POST方法,該欄位不是必選的,瀏覽器會默認允許這兩個方法進行跨域

舉例:

//www.huaweicloud.com,跨域訪問//portal.huaweicloud.com,HTTP方法為POST,如果伺服器響應里沒有Access-Control-Allow-Methods,跨域請求能夠成功:

如果伺服器響應里Access-Control-Allow-Methods不包含POST,跨域請求也能成功:

如果請求為PUT方法,但響應里沒有攜帶Access-Control-Allow-Methods或者取值不包含PUT,瀏覽器會報錯:

3.5.6 Access-Control-Allow-Headers(可選)

涉及非簡單Http請求

含義:該欄位指定了跨域允許設置的非簡單Http請求頭(5個簡單Http請求頭之外的頭域),(當預請求中包含 Access-Control-Request-Headers 時必須包含)– 這是對預請求當中 Access-Control-Request-Headers 的回復,和上面一樣是以逗號分隔的列表,可以返回所有支援的頭部。

舉例:

如果在XMLHttpRequest中設置了wise_groupid欄位,而伺服器響應中,沒有Access-Control-Allow-Headers頭域,或者Access-Control-Allow-Headers頭域的值不包含wise_groupid,則瀏覽器會報錯:

3.5.7 Access-Control-Max-Age(可選)

涉及簡單Http請求、非簡單Http請求

含義:用來指定本次預檢請求的有效期,單位為秒。例如,Access-Control-Max-Age被設置為1728000,表示有效期是20天(1728000秒),即允許快取該條回應1728000秒(即20天),在此期間,不用發出另一條預檢請求。

4 其它跨域手段

4.1 JSONP跨域

在HTML文檔中,有一個script標籤,該標籤一般會引用當前HTML文檔需要載入的js腳本,例如:

瀏覽器在載入HTML文檔時,會順序載入script標籤中src的地址,這種載入是可以跨域載入的,瀏覽器不會阻止跨域的js載入。

此外,還有img等標籤,可以跨域載入。

而 JSONP正是利用了script/img等標籤能夠跨域載入,來實現跨域請求的功能,如下程式碼所示:

function updateList (data) {
console.log(data);
}
$body.append(『
<script type=

程式碼先定義一個全局函數,然後把這個函數名通過callback參數添加到script標籤的src,script的src就是需要跨域的請求,然後這個請求返回可執行的JS文本:

// script響應返回的js內容為
updateList({status: "OK"});

由於它是一個js,並且已經定義了upldateList函數,所以能正常執行,並且跨域的數據通過傳參得到。這就是JSONP的原理。

如下圖所示,伺服器返回了響應之後,js方法updateList就可以獲取到響應內容,列印在控制台:

JSONP方式跨域訪問的時候,還會攜帶域名的Cookie:

 

從上面可以看到,JSONP方式跨域,會不受同源政策影響,並且會攜帶跨域域名的Cookie,同樣也會存在安全風險。

由於JSONP是利用script/img等標籤來實現跨域,而瀏覽器載入這些標籤,使用的是GET方法,這就要求業務對於一些重要的請求,不能夠使用GET方法提交數據,必須要使用POST方法,這樣就無法利用JSONP進行跨域請求了。

4.2 服務端轉發

如上圖所示,服務端轉發實現跨域訪問的基本原理是,將訪問B域名的請求,通過訪問A域名,由A伺服器轉發給B伺服器:

舉例:

//www.huaweicloud.com頁面上,想要訪問//portal.huaweicloud.com/v1/template介面,那麼可以通過如下手段實現跨域:

l  頁面上調用//www.huaweicloud.com/portal/v1/template介面

l  www.huaweicloud.com伺服器,對於/portal開頭的請求,統一去掉portal路徑,轉發給portal.huaweicloud.com伺服器,相當於www.huaweicloud.com伺服器做了反向代理

但是使用該方法,會導致無法發送portal.huaweicloud.com域名下的cookie,因此應用場景有限。

5 擴展

5.1 為什麼要區分簡單請求和非簡單請求?

按照上文介紹:

簡單請求就是滿足方法是GET、POST、HEAD,頭域為Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type,且Content-Type只限於三個值:application/x-www-form-urlencoded、multipart/form-data、text/plain的請求,比如普通的提交HTML Form表單的請求。

非簡單請求,就是普通 HTML Form 無法實現的請求。比如 PUT 方法、需要其他的內容編碼方式、自定義頭之類的。

對於伺服器來說:

第一,  許多伺服器壓根沒打算給跨站訪問使用,不會給CORS響應頭,瀏覽器也會做相應報錯,但是由於跨域訪問不會阻止請求發出,但是請求本身可能已經造成了後果,所以最好能夠阻止跨站請求發出。

第二,  要回答某個請求是否接受跨域訪問,可能涉及額外的計算邏輯,這個邏輯可能簡單,如一律放通;但也可能複雜,可能取決於哪個資源、哪種操作、來自哪個Origin。對於瀏覽器來說,它只需要知道能否跨域訪問,但是對於伺服器來說,計算成本可大可小。所以我們希望這種判斷不需要每次由伺服器進行計算。

CORS的預檢請求preflight就是這樣一種機制,瀏覽器先單獨請求一次,詢問伺服器某個資源是否可以跨站訪問,如果不允許的話,就在預檢請求的響應中告知瀏覽器,使得瀏覽器不再發送實際請求。

這個機制即為「先許可,再請求」,因此默認禁止了跨站請求。

如果允許的話,瀏覽器才會繼續發送實際請求,這樣不合法跨站請求就不會對伺服器造成任何影響。

但是這種機制,只能限於非簡單請求。在處理簡單請求的時候,如果伺服器不打算接受跨站請求,不能依賴CORS預檢請求preflight機制,因為普通表單會直接發起實際請求,所以默認禁止跨站的簡單請求是做不到的。

因此,我們常在安全規範中看到,不要使用GET方法來提交重要敏感數據,不要使用簡單的表單請求來提交敏感數據等,原因就在這裡。

6 總結

瀏覽器跨域有三大方式,AJAX請求跨域、JSONP跨域、服務端轉發跨域,每種跨域會適用於不同的業務場景。

其中,AJAX跨域使用場景較多,遵循W3C標準,由瀏覽器和伺服器根據HTTP頭域Access-Control開頭的相關欄位協商處理跨域流程。

HTTP請求還分為簡單請求和非簡單請求,在非簡單請求的跨域訪問時,還會觸發預檢請求preflight流程。

對於我們業務開發和測試的啟示:

對於重要敏感數據,不要使用GET、簡單表單提交等簡單HTTP請求來處理,需要使用非簡單請求來處理,這樣就沒法通過JSONP等跨域手段來攻擊獲取敏感數據。

此外,除了不使用簡單請求之外,還可以通過每次請求使用不同隨機串、增加驗證碼方式二次校驗等方法,來防止請求被跨站偽造。

7 參考資料

[1] 瀏覽器同源政策及其規避方法

[2] 徹底理解瀏覽器的跨域

[3] 為什麼跨域的post請求區分為簡單請求和非簡單請求和content-type相關?

[4] 淺談CSRF跨域攻擊 

 

點擊關注,第一時間了解華為雲新鮮技術~