JSON Web Token 是 rfc7519 出的一份標准,使用 JSON 來傳遞數據,用於判定用戶是否登錄狀態。
jwt 之前,使用 session
來做用戶認證。
以下代碼均使用 javascript 編寫。
- 原文鏈接: 山月的博客
session
傳統判斷是否登錄的方式是使用 session + token
。
token
是指在客戶端使用 token 作為用戶狀態憑證,瀏覽器一般存儲在 localStorage
或者 cookie
中。
session
是指在服務器端使用 redis 或者 sql 類數據庫,存儲 user_id 以及 token 的鍵值對關系,基本工作原理如下。
在服務器端使用 sessions
存儲鍵值對
const sessions = {
"ABCED1": 10086,
"CDEFA0": 10010
}
每次客戶端請求帶權限數據時攜帶 token,在服務器端根據 token 與 sessions 獲取 user_id, 完成認證過程
function getUserIdByToken (token) {
return sessions[token]
}
如果存儲在 cookie
中就是經常聽到的 session + cookie
的登錄方案。其實存儲在 cookie
,localStorage
甚至 IndexedDB
或者 WebSQL
各有利弊,核心思想一致。
關於 cookie
以及 token
優缺點,在 token authetication vs cookies 中有討論。
如果不使用 cookie,可以采取 localStorage + Authorization
的方式進行認證,更加無狀態化
// http 的頭,每次請求權限接口時,需要攜帶 Authorization Header
const headers = {
Authorization: `Bearer ${localStorage.get('token')}`
}
推薦一個前端的存儲庫 localForage,使用
IndexedDB
,WebSQL
以及IndexedDB
做鍵值對存儲。
無狀態登錄
session
需要在數據庫中保持用戶及token對應信息,所以叫 有狀態。
試想一下,如何在數據庫中不保持用戶狀態也可以登錄。
第一種方法: 前端直接傳 user_id 給服務端
缺點也特別特別明顯,容易被用戶篡改成任意 user_id,權限設置形同虛設。不過思路正確,接着往下走。
改進: 對 user_id 進行對稱加密
服務端對 user_id 進行對稱加密后,作為 token 返回客戶端,作為用戶狀態憑證。比上邊略微強點,但由於對稱加密,選擇合適的算法以及密鑰比較重要
改進: 對 user_id 不需要加密,只需要進行簽名,保證不被篡改
這便是 jwt 的思想:user_id,加密算法和簽名組成 token 一起存儲到客戶端,每當客戶端請求接口時攜帶 token,服務器根據 token 解析出加密算法與 user_id 來判斷簽名是否一致。
Json Web Token
jwt 根據 Header
,Payload
以及 Signature
三個部分由 .
拼接而成。
Header
Header 由非對稱加密算法和類型組成,如下
const header = {
// 加密算法
alg: 'HS256',
type: 'jwt'
}
Payload
Payload 中由 Registered Claim 以及需要通信的數據組成。這些數據字段也叫 Claim
。
Registered Claim
中比較重要的是 "exp" Claim
表示過期時間,在用戶登錄時會設置過期時間。
const payload = {
// 表示 jwt 創建時間
iat: 1532135735,
// 表示 jwt 過期時間
exp: 1532136735,
// 用戶 id,用以通信
user_id: 10086
}
Signature
Signature
由 Header
,Payload
以及 secretOrPrivateKey
計算而成。secretOrPrivateKey
作為敏感數據存儲在服務器端,可以考慮使用 vault secret
或者 k8s secret
對於 secretOrPrivateKey
,如果加密算法采用 HMAC
,則為字符串,如果采用 RSA
或者 ECDSA
,則為 PrivateKey。
// 由 HMACSHA256 算法進行簽名,secret 不能外泄
const sign = HMACSHA256(base64.encode(header) + '.' + base64.encode(payload), secret)
// jwt 由三部分拼接而成
const jwt = base64.encode(header) + '.' + base64.encode(payload) + '.' + sign
從生成 jwt 規則可知客戶端可以解析出 payload,因此不要在 payload 中攜帶敏感數據,比如用戶密碼
校驗過程
在生成規則中可知,jwt 前兩部分是對 header 以及 payload 的 base64 編碼。
當服務器收到客戶端的 token 后,解析前兩部分得到 header 以及 payload,並使用 header 中的算法與 secretOrPrivateKey 進行簽名,判斷與 jwt 中攜帶的簽名是否一致。
帶個問題,如何判斷 token 過期?
應用
由上可知,jwt 並不對數據進行加密,而是對數據進行簽名,保證不被篡改。除了在登錄中可以用到,在進行郵箱校驗,圖形驗證碼和短信驗證碼時也可以用到。
圖形驗證碼
在登錄時,輸入密碼錯誤次數過多會出現圖形驗證碼。
圖形驗證碼的原理是給客戶端一個圖形,並且在服務器端保存與這個圖片配對的字符串,以前也大都通過 session 來實現。
可以把驗證碼配對的字符串作為 secret,進行無狀態校驗。
const jwt = require('jsonwebtoken')
// 假設驗證碼為字符驗證碼,字符為 ACDE,10分鍾失效
const token = jwt.sign({}, secrect + 'ACDE', { expiresIn: 60 * 10 })
const codeImage = getImageFromString('ACDE')
// 給前端的響應
const res = {
// 驗證碼圖片的 token,從中可以校驗前端發送的驗證碼
token,
// 驗證碼圖片
codeImage,
}
短信驗證碼與圖形驗證碼同理
郵箱校驗
現在網站在注冊成功后會進行郵箱校驗,具體做法是給郵箱發一個鏈接,用戶點開鏈接校驗成功。
// 把郵箱以及用戶id綁定在一起
const code = jwt.sign({ email, userId }, secret, { expiresIn: 60 * 30 })
// 在此鏈接校驗驗證碼
const link = `https://example.com/code=${code}`
無狀態 VS 有狀態
關於無狀態和有狀態,在其它技術方向也有對比,比如 React 的 stateLess component
以及 stateful component
,函數式編程中的副作用可以理解為狀態,http 也是一個無狀態協議,需要靠 header 以及 cookie 攜帶狀態。
在用戶認證這里,有無狀態是指是否依賴外部數據存儲,如 mysql,redis 等。
案例
思考以下幾個關於登錄的問題如何使用 session 以及 jwt 實現,來更加清楚 jwt
的使用場景
當用戶注銷時,如何使該 token 失效
因為 jwt 無狀態,不保存用戶設備信息,沒法單純使用它完成以上問題,可以再利用數據庫保存一些狀態完成。
session
: 只需要把 user_id 對應的 token 清掉即可jwt
: 使用 redis,維護一張黑名單,用戶注銷時把該 token 加入黑名單,過期時間與 jwt 的過期時間保持一致。
如何允許用戶只能在一個設備登錄,如微信
session
: 使用 sql 類數據庫,對用戶數據庫表添加 token 字段並加索引,每次登陸重置 token 字段,每次請求需要權限接口時,根據 token 查找 user_idjwt
: 假使使用 sql 類數據庫,對用戶數據庫表添加 token 字段(不需要添加索引),每次登陸重置 token 字段,每次請求需要權限接口時,根據 jwt 獲取 user_id,根據 user_id 查用戶表獲取 token 判斷 token 是否一致。另外也可以使用計數器的方法,如下一個問題。
對於這個需求,session 稍微簡單些,畢竟 jwt 也需要依賴數據庫。
如何允許用戶只能在最近五個設備登錄,如諸多播放器
session
: 使用 sql 類數據庫,創建 token 數據庫表,有 id, token, user_id 三個字段,user 與 token 表為 1:m 關系。每次登錄添加一行記錄。根據 token 獲取 user_id,再根據 user_id 獲取該用戶有多少設備登錄,超過 5 個,則刪除最小 id 一行。jwt
: 使用計數器,使用 sql 類數據庫,在用戶表中添加字段 count,默認值為 0,每次登錄 count 字段自增1,每次登錄創建的 jwt 的 Payload 中攜帶數據 current_count 為用戶的 count 值。每次請求權限接口時,根據 jwt 獲取 count 以及 current_count,根據 user_id 查用戶表獲取 count,判斷與 current_count 差值是否小於 5
對於這個需求,jwt 略簡單些,而使用 session 還需要多維護一張 token 表。
如何允許用戶只能在最近五個設備登錄,而且使某一用戶踢掉除現有設備外的其它所有設備,如諸多播放器
session
: 在上一個問題的基礎上,刪掉該設備以外其它所有的token記錄。jwt
: 在上一個問題的基礎上,對 count + 5,並對該設備重新賦值為新的 count。
如何顯示該用戶登錄設備列表 / 如何踢掉特定用戶
session
: 在 token 表中新加列 devicejwt
: 需要服務器端保持設備列表信息,做法與 session 一樣,使用 jwt 意義不大
總結
從以上問題得知,如果不需要控制登錄設備數量以及設備信息,無狀態的 jwt 是一個不錯的選擇。一旦涉及到了設備信息,就需要對 jwt 添加額外的狀態支持,增加了認證的復雜度,此時選用 session 是一個不錯的選擇。
jwt 不是萬能的,是否采用 jwt,需要根據業務需求來確定。