作為一種新興的鑒權方案,Token 作為 Session ID 的替代品有許多天然優勢,很多主流產品也從 Session ID 鑒權變成了 Token 鑒權。在我近期完成的產品實踐中,也使用了 Token 進行鑒權。作為鑒權方案設計的參與者,我今天與負責該部分業務的同學進行了一些討論,同時咨詢了幾位業界老司機,結合我在產品設計之初的一些調研,總結出這篇文章分享給大家。
我不是安全專家,只是在能力范圍內對系統安全盡可能地做了優化,考慮不周的地方歡迎大家批評指正。
關于用戶系統
對于一個簡單的用戶系統(不考慮復雜的權限控制,只考慮最單一的“合法用戶”的鑒定),其功能其實可以被拆的很簡單:注冊、登錄、鑒權。
- 注冊:用戶將用戶名和密碼交給服務器,并由服務器存儲的過程。
- 登錄:用戶將用戶名和密碼交給服務器,服務器鑒定是否正確的過程(在 Token鑒權系統中,這一步如果通過,會生成并返回 Token)。
- 鑒權:用戶將 Token 發送給服務器,服務器校驗該 Token 是否合法的過程(不考慮復雜鑒權)。
安全問題
流程清楚了,我們就來分析一下問題。不考慮前端可能出現的網絡抓包等問題,僅從服務器角度考慮,我們可能遇到的安全問題有以下幾個:
- 密碼泄漏
- 生成 Token 的 Secret Key(Salt)泄漏
- Token 泄漏 / 偽造
歸納一下:我們要解決的最重要的安全問題,就是用戶最機密的安全信息被泄漏或偽造。
我們的鑒權設計實踐
在我最近完成的產品上,為了規避這些問題,我們在關鍵步驟上進行了一些處理。整個鑒權系統依賴 Apache Shiro 框架;同時,在密碼處理,Token 認證上,我們結合了一些自己的解決方案。
整個流程大致是這樣的:(流程圖軟件到期了 TAT)
注冊
登錄
鑒權
關于密碼加密存儲與驗證
密碼是一定要進行加密存儲的。用戶系統最核心的數據表,就是包含用戶名(ID)、加密后的密碼、Salt 的表。Salt 的生成,我們使用了 Shiro 提供的隨機字符串生成工具,與用戶名連接后,再進行 MD5。然后使用 Salt 加密密碼,然后同時保存 Salt 和加密后的密碼。
當用戶登錄時,我們使用 Salt 對用戶輸入的密碼進行加密,再嘗試與存儲的密碼進行匹配。
關于 Token 方案(JWT Token)
我們使用 JWT Token 作為我們的 Token 方案。
JWT Token
JWT Token 的全稱是 JSON Web Token。一個 JWT Token 由三部分構成:Header,Payload,Signature。Header 規定了 Token 使用的加密方式與 Token 的類型,Payload 是 Token 中包含的用戶信息(用戶名,過期時間等),Signature 是 Header 的 Base64 值 + Payload 的 Base64 值 + Secret Key 生成的字符串,再對該字符串使用 Header 中規定的散列方式(HS256 或 RS256)取散列值后得到的字符串。一個典型的 JWT Token 是這個樣子的:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
// HEADER
{
"alg": "HS256",
"typ": "JWT"
}
// PAYLOAD
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
// SIGNATURE
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Header、Payload 和 Signature 用 .
分隔。
驗證 Token 的時候,我們只需要將前兩段(即 Header 和 Payload 的 Base64)加上 Secret Key,然后按照 Header 規定的加密方式進行加密,將生成的字符串與第三段(Signature)比對即可。當然,Token 驗證的實踐上,不同的項目存在一些分歧:有些人會將生成的 Token 直接存在數據庫(比如 Redis)里,然后通過 Query 的方式驗證是否合法。這一點我們隨后討論。
一個 JWT Token 唯一不可見的部分,就是 Secret Key。它是保證這個 Token 合法且安全的唯一字段。拿不到 Secret Key ,就無法生成 Token,也無法驗證 Token。這種 Token 機制很常見(HTTPS 的握手過程就類似這樣,SSH 連接也是 - 私鑰只有一方持有),難點在于,如何生成并管理 Secret Key。
Secret Key
首先,所有用戶使用相同的 Secret Key 一定是不合理的。所以我們要解決的第一個問題是,如何為每一個用戶生成唯一的 Secret Key ?
還記得剛才的 Salt 么?每個用戶的 Salt 都是唯一的,我們使用 Salt ,但不直接使用 Salt 作為 Secret Key。我們使用 Salt + 加密后的密碼,再取 MD5 值作為該用戶的 Secret Key。每次鑒權前,我們通過這個方式生成 Secret Key,再使用 Secret Key 進行鑒權。
安全性分析
整套系統的安全之處在于,我們沒有將任何敏感信息本地化。假設一種最壞的情況:攻擊者拿到了我們數據庫的全部數據,他能做什么?
- 獲取密碼:密碼被加密了,而且每個用戶使用不同的 Salt 進行加密,加密方法是自定義的,不知道加密方法的話難以破解。
- 獲取 Token:我們沒有保存任何的 Token。
- 獲取 Secret Key:Secret Key 是算出來的,即便拿到了 Salt,不知道算法也無法直接得到 Secret Key。
我們避免了直接保存任何安全信息。攻擊者拿到的數據,都無法被直接利用。即便嘗試破解,代價也是巨大的。
關于 Redis
在我看到的一些實踐中,有些項目喜歡使用 Redis 存儲生成的 Token,從而簡化鑒權流程,提升鑒權效率。這樣做可以嗎?
我咨詢了一位業界專家,同時查閱了相關資料,我給出的答案是:可以,但是不合理,不推薦。
避免用 Redis 直接存儲 Token
還記得我們安全性分析的前提么:如果攻擊者拿到了我們數據庫的全部數據,他能做什么?
將 Token 保存在 Redis 中,一定是有風險的。如果服務器被攻破,用戶 Token 泄漏的話,在規定的過期時間內,這些被泄漏的 Token 將會使用戶賬戶變得非常危險。
當然,如果系統運行在內網環境,或者系統本身對用戶安全的要求不高,這種方案從某種程度上講,確實可以提升鑒權效率,簡化鑒權流程。但是鑒于其可能存在的安全問題,?我不推薦。
可以用 Redis 緩存 Salt
在我們的產品設計中,我們使用 Salt 計算 Secret Key,然后再進行 Token 認證。我們可以在用戶登錄時把 Salt 緩存到 Redis 中以提升查詢效率。
進一步優化
使用 Payload 生成 Secret Key
現在,整個系統的安全性基本可靠了。但是,仔細分析系統的設計,還是有一點問題:每次鑒權都需要去查詢 Salt,I/O 開銷比較大。這恰恰也是有些人使用 Redis 的原因之一 —— 提升查詢速度。
仔細分析一下,我們用 Salt 當做了生成 Secret Key 的 Seed ,目的在于保證 Secret Key 唯一,同時不直接存儲 Secret Key 。但其實,保持 Secret Key 唯一的方式有很多,不一定要通過 Salt 。實際上只有登錄操作必須依賴 Salt,鑒權操作完全可以使用別的機制。
我們可以使用 JWT 的 Payload 中的某些字段,通過特定算法生成 Secret Key。比如:有效期時間戳 + 用戶名,再取 SHA256 散列值(當然可以更復雜,不過要注意性能開銷)。因為生成 Secret Key 的算法是不透明的,所以 Secret Key 也是相對安全的。
如果對把生成 Token 的信息放在 Payload 中心存顧慮的話,我們可以在服務器上通過靜態配置文件的方式設置固定的 Secret Salt ,配合 Payload 生成 Secret Key。
通過這樣的方式,我們可以避免在鑒權階段對數據庫進行訪問,提升響應效率。我們也可以利用 Secret Salt 進行細粒度的權限角色劃分,在此就不贅述了。
更標準的密碼加密模式
關于密碼加密等方式,我的老師給了我一些建議:可以使用 Blowfish 算法進行對稱加密。這樣的加密更標準,更安全。
JWT Token 與前端
JWT Token 應該放在哪
官方建議使用 Bearer 的模式,即:
Authorization: Bearer <token>
合理使用 Payload,避免 Token 過長
JWT Token 是有 Payload 的,這從一定程度上會造成 Payload 濫用。我在 Chrome 上遇到一個奇怪的 Bug:如果 Authorization 過長,Chrome 傳遞這個字段的時候會發生截斷。我們的產品剛開始研發的時候,過度依賴 JWT 的 Payload 傳遞用戶基本信息(用戶名、所屬用戶組、郵箱等),造成 Token 長度非常長。后來對 Token 進行了幾次瘦身,才避免了 Chrome 上的 Bug。
前端真的需要依賴 Payload 嗎?
答案是否定的。前端并不關心,也不應該關心 Token 的 Payload 是什么,真正使用 Payload 的應該是后端。前端獲取用戶信息的方式,應當是在用戶登錄的時候,由服務器作為 HTTP Response 回傳,并使用 Cookie / Local Storage / Session Storage 進行持久化存儲,而不是通過解析 Token 的 Payload 獲得。
以上就是我對 Web App Token 鑒權方案的一些思考。以下是有關 Web App Token 認證的文檔:
RFC 6749: The OAuth 2.0 Authorization Framework
RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage
RFC 7519 JSON Web Token (JWT)