安全開發之 token 那些事

晚風 合天智匯

在開發網絡應用時,不管是移動端的 APP 也好,還是 web 端 APP 也好,只要有用戶群體存在,都繞不開身份認證這個話題,選擇一種好的身份認證方法常常在應用安全中起到了至關重要的作用。目前用的最多的就是使用“token”認證用戶的身份。

本文主要講述了你不知道的關於 token 的那些事,以及在目前常見的應用中增加 token 認證的方案。

token 的作用

token 是什麼?簡單來說 token 就是在客戶端與服務器之間傳輸的一段字符串。

說到 token 的作用,那這裡不得不提一下 CSRF 攻擊。CSRF 全稱“跨站請求偽造攻擊”。假設一個用戶登錄一個銀行網站,此時銀行網站將用戶的登錄狀態保存在了瀏覽器的 cookie 中,每當用戶訪問這個銀行網站的不同頁面時,瀏覽器會自動帶上 cookie 中用戶的登錄狀態,服務器以此來判斷用戶登錄與否,並根據用戶的登錄狀態響應不同的結果。

此時,攻擊者寫了一個惡意頁面,內含一個指示銀行網站從用戶賬號向攻擊者賬號轉錢的請求,並誘使用戶訪問這個攻擊者寫的惡意頁面。一旦用戶訪問了這個惡意頁面,該惡意請求將自動帶著 cookie 中用戶的登錄狀態被髮送到銀行網站的服務器上,銀行服務器認為這個請求是用戶自己發出的,就執行了該請求,從用戶的賬號向攻擊者的賬號轉了相應數額的錢,給用戶造成了損失。

CSRF 攻擊大體上就是這個樣子,不過 CSRF 不是本文講述的重點,有興趣可以自行了解 CSRF 的細節部分。

token 就是用來區別請求是來自用戶本身還是他人偽造的一個好辦法。當用戶在登錄時,服務器生成一個 token 發送給客戶端,客戶端把這個 token 存在內存中或者本地,每次請求都帶上這個 token,服務器接收到這個 token 並驗證合法性,合法即繼續執行請求,非法即攔截請求,不予執行。

由於瀏覽器的同源策略的限制,攻擊者的頁面無法跨域得到用戶頁面接收到的 token,所以攻擊者的請求肯定是無法給出合法的 token 的(排除 token 被盜的可能,token 被盜不是本文討論的範疇),由此服務器可以判斷請求到底是用戶自己發出的,還是以用戶的名義被偽造發出的。從而防範 CSRF 攻擊。

token 在開發中的實踐

1、前後端混合開發

使用前後端混合開發模式是較為傳統的開發模式。一般是後端寫完功能讓前端寫樣式,前後端共同維護著同一個頁面。在這種應用中,session 會話就挑起了客戶端與服務端通信的大旗。請求一般以 form 表單的形式發送給服務器。在這種應用中加上 token 進行身份驗證常見的有兩種方案。

方案一:服務端 token+ 表單頁面 token

在用戶輸入正確的用戶名和密碼登錄成功後,由服務器生成 token,一份存入 session 中,以 PHP 為例:

$_SESSION['token'] =generateToken();

一份存入頁面中的表單,在頁面上所有的表單中加入一個存放 token 的隱藏域:

...

...

在表單提交上來時先檢查接收到的 token 是否與 session 中的 token 相等,相等即可證明請求是來自用戶自己,不相等則該請求很可能並非來自用戶本身,很可能用戶遭到了 CSRF 攻擊。

方案二:cookie 中 token+ 表單頁面 token

在用戶登錄成功後服務器生成 token,一份同上存入表單頁面的隱藏域中,一份存入用戶 cookie,以 PHP 為例如下:

setcookie("token",generateToken(),time()+3600,'','','',true);

同樣的,當服務端接收到請求時,比對 cookie 中的 token 和表單中的 token 是否相等,相等則合法,反之非法。這種方案的優勢在於服務器端可以不存放用戶的登錄狀態,節約了服務器的資源,也算是順應了 http 的無狀態。

2、前後端分離開發

使用前後端分離的開發模式是較為新穎的開發模式。這種開發模式一般是前端與後端先協商好一份 Restful API 文檔,標明請求的地址、格式、類型等,然後各自對照著這份 api 文檔同時進行開發,提升了效率。這種開發模式在目前流行的單頁應用(SPA)中使用較多。在這種應用中可以不使用 session 會話來維持客戶端與服務器的通信。轉而只用 JWT(Json Web Token)來實現身份認證。

https://jwt.io/introduction/

單頁應用為了維護其良好的用戶體驗,發送請求的方式由傳統的 form 表單提交改為了使用 AJAX/Fetch 傳輸數據,實現頁面無刷。用戶在登錄成功接收到 token 後可以將 token 存在內存中,也就是可以存在一個 JS 全局變量裡,也可以存在 LocalStorage 中,唯一的區別是後者可以實現自動登錄而前者不可以。每次發送請求時將 base64 編碼後的 token 添加到 header 裡的 Authorization 中發送給服務器:

//ajax

$.ajax({

type: 'POST',

url: '/api/datapost',

data: {

title: '...',

content: '...'

},

headers: {

Authorization: 'dG9rZW5IZXJl'

},

success: function(res) {

do(res);

}

})

//fetch

fetch('/api/datapost',{

method: 'POST',

headers: {

"Content-Type": "application/json",

"Authorization": 'dG9rZW5IZXJl'

},

body: JSON.stringify(

{

title: '...',

content: '...'

}

)

}).then(res=>do(res))

.catch(e=>handle(e))

服務器解密驗證 headers 中的 JWT,得到身份數據。整個流程如下:

安全開發之 token 那些事

大家可以想想為什麼前兩種方案都需要驗證兩個 token 是否相等來判斷 token 的合法性而這裡不需要。

這是因為攻擊者如果要利用 CSRF,構造一個包含惡意請求的頁面,無論 GET 還是 POST 還是別的請求類型,由於同源策略的限制,請求只能由構造 form 表單發出,AJAX 是不支持跨域發送請求的(除非服務器開啟跨域支持,如果服務器開啟跨域,開發者需要嚴格限制請求的來源,對不信任的來源不予響應),而通過表單發送的請求是沒法添加自定義的 header 頭的,也就是說攻擊者是發不出 header 中帶有 token 的請求,所以我們可以以此來進行身份認證。

這種方案的優勢在於服務器保持無狀態,不需要維持用戶的登錄狀態,給服務器節約了資源。而且在一些無法使用 cookie 的場景下也適用。

token 的生成方法

其實 csrf token 就是一段隨機值而已,它的實現方法因人而異,不同的公司可能有不同的標準,可以使用標準的 JWT 格式,也可以是內部約定的實現方法,但總的來說要滿足隨機性,不能輕易被別人預測到;字符數不能太長也不能太短,太短容易被碰撞出來,太長給傳輸帶來不便,耗費資源影響速度。下面分別以 PHP 和 JAVA 為例

PHP:使用 uniqid() 方法生成隨機值,開啟第二個參數增加一個熵,使生成的結果更具唯一性,應對高併發

functiongenerateToken() {

returnmd5(uniqid(microtime() . mt_rand(),true));

}

JAVA: 使用 java.util 包下面的 UUID 類中的 randomUUID() 方法

publicstaticStringgenerateToken()

{

returnUUID.randomUUID().toString().replace("-", "");

}

當然了,具體的實現方案還要按照各自的業務需求來,這裡只是介紹了幾種常見的 token 方案僅供參考。


分享到:


相關文章: