跨域詳解

最近淺入前端的坑,把一些剛入門的小坑填上,用於後續回顧。

日常的前端開發中,不免會需要進行跨域操作,而在實際進行跨域請求時,經常會遇到類似

這樣的報錯。通常,這樣的錯誤是由於CORS跨域驗證機制設置不正確導致的。

根據同源策略,瀏覽器默認是不允許XMLHttpRequest對象問非同一站點下的資源的,即用ajax方式訪問非同一域名下的資源會出錯。比如當google要通過ajax去訪問百度的數據,是不行的。

禁用跨域訪問資源是爲了安全,但會犧牲便利性。因此就出現了好幾種跨域訪問資源的方法。其中一種是稱之爲CORS的技術(Cross Origin Resourse-Sharing,跨域資源共享)。

1. 什麼是CORS

所謂CORS,是指通過XMLHttpRequest的ajax方式訪問其他域名下資源,而不是在A域名的頁面上點擊打開一個B域名的頁面。引入不同域上的js腳本文件也是沒問題的。出於安全考慮,跨域請求不能訪問document.cookie對象。

當一個請求url的協議、域名、端口三者之間任意一與當前頁面地址不同即爲跨域。

比如 http://www.aaa.com 和下列URL相比,都不屬於同源。

https://www.aaa.com

http://www.aaa.com:8080

http://aaa.com

但下面這種屬於同源:

http://username:password@www.aaa.com

例如最常見的,在一個域名下的網頁上,調用另一個域名中的資源。

實際效果演示,讀者可以點擊這裏,然後打開開發者工具,查看報錯信息。

CORS技術允許跨域訪問多種資源,比如javascript,字體文件等,這種技術對XMLHttpRequest做了升級,使之可以進行跨域訪問。但不是所有的瀏覽器都支持CORS技術。Firefox和Chrome等瀏覽器支持的比較好,稍微新一點的版本都支持。IE比較搓,IE10才真正支持這個機制,IE10以下需要用XDomainRequest這個對象,這是IE特有的。

當然不是說瀏覽器支持了就立刻可以跨域訪問了,CORS技術中最重要的關鍵點是響應頭裏的Access-Control-Allow-Origin這個Header。 此Header是W3C標準定義的用來檢查是可否接受跨域請求的一個標識,下文會詳情說明。

2. CORS的作用

爲了改善網絡應用程序,開發人員要求瀏覽器供應商允許跨域請求。跨域請求主要用於:

  • 調用XMLHttpRequest或fetchAPI通過跨域方式訪問資源。
  • 網絡字體,例如Bootstrap(通過CSS使用@font-face跨域調用字體)
  • 通過canvas標籤繪製圖表和視頻。

3. CSRF

3.1 CSRF原理

跨域請求和ajax技術都會極大地提高頁面的體驗,但同時也會帶來安全的隱患,其中最主要的隱患來自於CSRF(Cross-Site Request Forgery,跨站請求僞造),也被稱爲 “one click attack” 或者 session riding,通常縮寫爲 CSRF 或者 XSRF,是一種對網站的惡意利用。CSRF 則通過僞裝來自受信任用戶的請求來利用受信任的網站。

CSRF攻擊的大致原理如下:

  1. 用戶打開瀏覽器,訪問受信任網站A,輸入用戶名和密碼請求登錄網站A;
  2. 在用戶信息通過驗證後,網站A產生Cookie信息並返回給瀏覽器,此時用戶登錄網站A成功,可以正常發送請求到網站A;
  3. 用戶未退出網站A之前,在同一瀏覽器中,打開一個TAB頁訪問網站B;
  4. 網站B接收到用戶請求後,返回一些攻擊性代碼,併發出一個請求要求訪問第三方站點A;
  5. 瀏覽器在接收到這些攻擊性代碼後,根據網站B的請求,在用戶不知情的情況下攜帶Cookie信息,向網站A發出請求。網站A並不知道該請求其實是由B發起的,所以會根據用戶C的Cookie信息以C的權限處理該請求,導致來自網站B的惡意代碼被執行。

3.2 當前防範CSRF的幾種策略

在業界目前防禦 CSRF 攻擊主要有三種策略:驗證 HTTP Referer 字段;在請求地址中添加 token 並驗證;在 HTTP 頭中自定義屬性並驗證。下面就分別對這三種策略進行詳細介紹。

3.2.1 驗證 HTTP Referer 字段

根據 HTTP 協議,在 HTTP 頭中有一個字段叫 Referer,它記錄了該 HTTP 請求的來源地址。在通常情況下,訪問一個安全受限頁面的請求來自於同一個網站,比如需要訪問 http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory,用戶必須先登陸 bank.example,然後通過點擊頁面上的按鈕來觸發轉賬事件。這時,該轉帳請求的 Referer 值就會是轉賬按鈕所在的頁面的 URL,通常是以 bank.example 域名開頭的地址。而如果黑客要對銀行網站實施 CSRF 攻擊,他只能在他自己的網站構造請求,當用戶通過黑客的網站發送請求到銀行時,該請求的 Referer 是指向黑客自己的網站。因此,要防禦 CSRF 攻擊,銀行網站只需要對於每一個轉賬請求驗證其 Referer 值,如果是以 bank.example 開頭的域名,則說明該請求是來自銀行網站自己的請求,是合法的。如果 Referer 是其他網站的話,則有可能是黑客的 CSRF 攻擊,拒絕該請求。

這種方法的顯而易見的好處就是簡單易行,網站的普通開發人員不需要操心 CSRF 的漏洞,只需要在最後給所有安全敏感的請求統一增加一個攔截器來檢查 Referer 的值就可以。特別是對於當前現有的系統,不需要改變當前系統的任何已有代碼和邏輯,沒有風險,非常便捷。

然而,這種方法並非萬無一失。Referer 的值是由瀏覽器提供的,雖然 HTTP 協議上有明確的要求,但是每個瀏覽器對於 Referer 的具體實現可能有差別,並不能保證瀏覽器自身沒有安全漏洞。使用驗證 Referer 值的方法,就是把安全性都依賴於第三方(即瀏覽器)來保障,從理論上來講,這樣並不安全。事實上,對於某些瀏覽器,比如 IE6 或 FF2,目前已經有一些方法可以篡改 Referer 值。如果 bank.example 網站支持 IE6 瀏覽器,黑客完全可以把用戶瀏覽器的 Referer 值設爲以 bank.example 域名開頭的地址,這樣就可以通過驗證,從而進行 CSRF 攻擊。

即便是使用最新的瀏覽器,黑客無法篡改 Referer 值,這種方法仍然有問題。因爲 Referer 值會記錄下用戶的訪問來源,有些用戶認爲這樣會侵犯到他們自己的隱私權,特別是有些組織擔心 Referer 值會把組織內網中的某些信息泄露到外網中。因此,用戶自己可以設置瀏覽器使其在發送請求時不再提供 Referer。當他們正常訪問銀行網站時,網站會因爲請求沒有 Referer 值而認爲是 CSRF 攻擊,拒絕合法用戶的訪問。

3.2.2 在請求地址中添加 token 並驗證(目前主流方法)

CSRF 攻擊之所以能夠成功,是因爲黑客可以完全僞造用戶的請求,該請求中所有的用戶驗證信息都是存在於 cookie 中,因此黑客可以在不知道這些驗證信息的情況下直接利用用戶自己的 cookie 來通過安全驗證。要抵禦 CSRF,關鍵在於在請求中放入黑客所不能僞造的信息,並且該信息不存在於 cookie 之中。可以在 HTTP 請求中以參數的形式加入一個隨機產生的 token,並在服務器端建立一個攔截器來驗證這個 token,如果請求中沒有 token 或者 token 內容不正確,則認爲可能是 CSRF 攻擊而拒絕該請求。

這種方法要比檢查 Referer 要安全一些,token 可以在用戶登陸後產生並放於 session 之中,然後在每次請求時把 token 從 session 中拿出,與請求中的 token 進行比對,但這種方法的難點在於如何把 token 以參數的形式加入請求。對於 GET 請求,token 將附在請求地址之後,這樣 URL 就變成 http://url?csrftoken=tokenvalue。 而對於 POST 請求來說,要在 form 的最後加上 <input type=”hidden” name=”csrftoken” value=”tokenvalue”/>,這樣就把 token 以參數的形式加入請求了。但是,在一個網站中,可以接受請求的地方非常多,要對於每一個請求都加上 token 是很麻煩的,並且很容易漏掉,通常使用的方法就是在每次頁面加載時,使用 javascript 遍歷整個 dom 樹,對於 dom 中所有的 a 和 form 標籤後加入 token。這樣可以解決大部分的請求,但是對於在頁面加載之後動態生成的 html 代碼,這種方法就沒有作用,還需要程序員在編碼時手動添加 token。

該方法還有一個缺點是難以保證 token 本身的安全。特別是在一些論壇之類支持用戶自己發表內容的網站,黑客可以在上面發佈自己個人網站的地址。由於系統也會在這個地址後面加上 token,黑客可以在自己的網站上得到這個 token,並馬上就可以發動 CSRF 攻擊。爲了避免這一點,系統可以在添加 token 的時候增加一個判斷,如果這個鏈接是鏈到自己本站的,就在後面添加 token,如果是通向外網則不加。不過,即使這個 csrftoken 不以參數的形式附加在請求之中,黑客的網站也同樣可以通過 Referer 來得到這個 token 值以發動 CSRF 攻擊。這也是一些用戶喜歡手動關閉瀏覽器 Referer 功能的原因。

CSRF攻擊是黑客藉助受害者的 cookie 騙取服務器的信任,但是黑客並不能拿到 cookie,也看不到 cookie
的內容。另外,對於服務器返回的結果,由於瀏覽器同源策略的限制,黑客也無法進行解析。因此,黑客無法從返回的結果中得到任何東西,他所能做的就是給服務器發送請求,以執行請求中所描述的命令,在服務器端直接改變數據的值,而非竊取服務器中的數據。所以,我們要保護的對象是那些可以直接產生數據改變的服務,而對於讀取數據的服務,則不需要進行CSRF 的保護。比如銀行系統中轉賬的請求會直接改變賬戶的金額,會遭到 CSRF攻擊,需要保護。而查詢餘額是對金額的讀取操作,不會改變數據,CSRF 攻擊無法解析服務器返回的結果,無需保護。

由此,GET 請求不需要 CSRF Token(因爲寫操作不應該用 GET),POST 請求在執行寫操作時依舊需要 CSRF Token。

3.2.3 在 HTTP 頭中自定義屬性並驗證

這種方法也是使用 token 並進行驗證,和上一種方法不同的是,這裏並不是把 token 以參數的形式置於 HTTP 請求之中,而是把它放到 HTTP 頭中自定義的屬性裏。通過 XMLHttpRequest 這個類,可以一次性給所有該類請求加上 csrftoken 這個 HTTP 頭屬性,並把 token 值放入其中。這樣解決了上種方法在請求中加入 token 的不便,同時,通過 XMLHttpRequest 請求的地址不會被記錄到瀏覽器的地址欄,也不用擔心 token 會透過 Referer 泄露到其他網站中去。

然而這種方法的侷限性非常大。XMLHttpRequest 請求通常用於 Ajax 方法中對於頁面局部的異步刷新,並非所有的請求都適合用這個類來發起,而且通過該類請求得到的頁面不能被瀏覽器所記錄下,從而進行前進,後退,刷新,收藏等操作,給用戶帶來不便。另外,對於沒有進行 CSRF 防護的遺留系統來說,要採用這種方法來進行防護,要把所有請求都改爲 XMLHttpRequest 請求,這樣幾乎是要重寫整個網站,這代價無疑是不能接受的。

4. CORS驗證機制

出於安全原因,瀏覽器限制從腳本中發起的跨域HTTP請求。默認的安全限制爲同源策略, 即JavaScript或Cookie只能訪問同域下的內容。
W3C推薦了一種跨域的訪問驗證的機制,即CORS(Cross-Origin Resource Sharing 跨源資源共享)。
這種機制讓Web應用服務器能支持跨站訪問控制,使跨站數據傳輸更加安全,減輕跨域HTTP請求的風險。
CORS驗證機制需要客戶端和服務端協同處理。

5. 客戶端處理機制

基於上述的CSRF的風險,各主流的瀏覽器都會對動態的跨域請求進行特殊的驗證處理。驗證處理分爲簡單請求驗證處理和預先請求驗證處理。

5.1 簡單請求

當請求同時滿足下面兩個條件時,瀏覽器會直接發送GET請求,在同一個請求中做跨域權限的驗證。

請求方法是下列之一:

  • GET
  • HEAD
  • POST

請求頭中的Content-Type請求頭的值是下列之一:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

簡單請求時,瀏覽器會直接發送跨域請求,並在請求頭中攜帶Origin 的header,表明這是一個跨域的請求。
服務器端接到請求後,會根據自己的跨域規則,通過Access-Control-Allow-Origin和Access-Control-Allow-Methods響應頭,來返回驗證結果。
如果驗證成功,則會直接返回訪問的資源內容。

如果驗證失敗,則返回403的狀態碼,不會返回跨域請求的資源內容。

可以通過瀏覽器的Console查看具體的驗證失敗原因

5.2 預先請求

當請求滿足下面任意一個條件時,瀏覽器會先發送一個OPTION請求,用來與目標域名服務器協商決定是否可以發送實際的跨域請求。

請求方法不是下列之一:

  • GET
  • HEAD
  • POST

請求頭中的Content-Type請求頭的值不是下列之一:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

瀏覽器在發現頁面中有上述條件的動態跨域請求的時候,並不會立即執行對應的請求代碼,而是會先發送Preflighted requests(預先驗證請求),Preflighted requests是一個OPTION請求,用於詢問要被跨域訪問的服務器,是否允許當前域名下的頁面發送跨域的請求。 

OPTIONS請求頭部中會包含以下頭部:Origin、Access-Control-Request-Method、Access-Control-Request-Headers。
服務器收到OPTIONS請求後,設置Access-Control-Allow-Origin、Access-Control-Allow-Method、Access-Control-Allow-Headers頭部與瀏覽器溝通來判斷是否允許這個請求。
如果Preflighted requests驗證通過,瀏覽器纔會發送真正的跨域請求。

如果Preflighted requests驗證失敗,則會返回403狀態,瀏覽器不會發送真正的跨域請求。

可以通過瀏覽器的Console查看具體的驗證失敗原因

5.3 帶認證的請求

默認情況下,跨源請求不提供憑據(cookie、HTTP認證及客戶端SSL證明等)。通過將withCredentials屬性設置爲true,可以指定某個請求應該發送憑據。

xhr.withCredentials = true;

如果服務器接收帶憑據的請求,會用下面的HTTP頭部來響應。
Access-Control-Allow-Credentials: true
服務器還可以在Preflight響應中發送這個HTTP頭部,表示允許源發送帶憑據的請求。

如果發送的是帶憑據的請求,但服務器的響應中沒有包含這個頭,那麼瀏覽器就不會把響應交給JavaScript(responseText中將是空字符串,size爲0)。

注意,當withCredentials屬性設置爲true,需要response header中的'Access-Control-Allow-Origin'爲一個確定的域名,而不能使用'*'這樣的通配符。

6. 服務端處理機制

服務器端對於跨域請求的處理流程如下:

  1. 首先查看http頭部有無origin字段;
  2. 如果沒有,或者不允許,直接當成普通請求處理,結束;
  3. 如果有並且是允許的,那麼再看是否是preflight(method=OPTIONS);
  4. 如果不是preflight(簡單請求),就返回Allow-Origin、Allow-Credentials等,並返回正常內容。
  5. 如果是preflight(預先請求),就返回Allow-Headers、Allow-Methods等,內容爲空;

7. HTTP Header

7.1 Request header

7.1.1 Origin

Origin頭在跨域請求或預先請求中,標明發起跨域請求的源域名。

7.1.2 Access-Control-Request-Method

Access-Control-Request-Method頭用於表明跨域請求使用的實際HTTP方法

7.1.3 Access-Control-Request-Headers

Access-Control-Request-Headers用於在預先請求時,告知服務器要發起的跨域請求中會攜帶的請求頭信息

7.2 Response header

7.2.1 Access-Control-Allow-Origin

Access-Control-Allow-Origin頭中攜帶了服務器端驗證後的允許的跨域請求域名,可以是一個具體的域名或是一個*(表示任意域名)。簡單請求時,瀏覽器會根據此響應頭的內容決定是否給腳本返回相應內容,預先驗證請求時,瀏覽器會根據此響應頭決定是否發送實際的跨域請求。

7.2.2 Access-Control-Expose-Headers

Access-Control-Expose-Headers頭用於允許返回給跨域請求的響應頭列表,在列表中的響應頭的內容,纔可以被瀏覽器訪問。

7.2.3 Access-Control-Max-Age

Access-Control-Max-Age用於告知瀏覽器可以將預先檢查請求返回結果緩存的時間,在緩存有效期內,瀏覽器會使用緩存的預先檢查結果判斷是否發送跨域請求。

7.2.4 Access-Control-Allow-Credentials

Access-Control-Allow-Credentials用於告知瀏覽器當withCredentials屬性設置爲true時,是否可以顯示跨域請求返回的內容。簡單請求時,瀏覽器會根據此響應頭決定是否顯示響應的內容。預先驗證請求時,瀏覽器會根據此響應頭決定在發送實際跨域請求時,是否攜帶認證信息。

7.2.5 Access-Control-Allow-Methods

Access-Control-Allow-Methods用於告知瀏覽器可以在實際發送跨域請求時,可以支持的請求方法,可以是一個具體的方法列表或是一個*(表示任意方法)。簡單請求時,瀏覽器會根據此響應頭的內容決定是否給腳本返回相應內容,預先驗證請求時,瀏覽器會根據此響應頭決定是否發送實際的跨域請求。

7.2.6 Access-Control-Allow-Headers

Access-Control-Allow-Headers用於告知瀏覽器可以在實際發送跨域請求時,可以支持的請求頭,可以是一個具體的請求頭列表或是一個*(表示任意請求頭)。簡單請求時,瀏覽器會根據此響應頭的內容決定是否給腳本返回相應內容,預先驗證請求時,瀏覽器會根據此響應頭決定是否發送實際的跨域請求。 

8. 配置CORS規則示例

nginx上的CORS配置

Reference:

1. http://www.tuicool.com/articles/7FVnMz

2. http://cnn237111.blog.51cto.com/2359144/1610917?utm_source=tuicool&utm_medium=referral

3. https://segmentfault.com/q/1010000000713614

4. https://www.ibm.com/developerworks/cn/web/1102_niugang_csrf/

5. https://yq.aliyun.com/articles/69313

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章