jwt 實踐應用以及特殊案例思考

JSON Web Token 是 rfc7519 出的一份標準,使用 JSON 來傳遞數據,用於斷定用戶是否登陸狀態。javascript

jwt 以前,使用 session 來作用戶認證。html

如下代碼均使用 javascript 編寫。

session

傳統判斷是否登陸的方式是使用 session + token前端

token 是指在客戶端使用 token 做爲用戶狀態憑證,瀏覽器通常存儲在 localStorage 或者 cookie 中。java

session 是指在服務器端使用 redis 或者 sql 類數據庫,存儲 user_id 以及 token 的鍵值對關係,基本工做原理以下。mysql

在服務器端使用 sessions 存儲鍵值對git

const sessions = {
  "ABCED1": 10086,
  "CDEFA0": 10010
}

每次客戶端請求帶權限數據時攜帶 token,在服務器端根據 token 與 sessions 獲取 user_id, 完成認證過程程序員

function getUserIdByToken (token) {
  return sessions[token]
}

若是存儲在 cookie 中就是常常聽到的 session + cookie 的登陸方案。其實存儲在 cookielocalStorage 甚至 IndexedDB 或者 WebSQL 各有利弊,核心思想一致。github

關於 cookie 以及 token 優缺點,在 token authetication vs cookies 中有討論。web

若是不使用 cookie,能夠採起 localStorage + Authorization 的方式進行認證,更加無狀態化面試

// http 的頭,每次請求權限接口時,須要攜帶 Authorization Header
const headers = {
  Authorization: `Bearer ${localStorage.get('token')}`
}
推薦一個前端的存儲庫 localForage,使用 IndexedDBWebSQL 以及 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 根據 HeaderPayload 以及 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

SignatureHeaderPayload 以及 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_id
  • jwt: 假使使用 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 表中新加列 device
  • jwt: 須要服務器端保持設備列表信息,作法與 session 同樣,使用 jwt 意義不大

總結

從以上問題得知,若是不須要控制登陸設備數量以及設備信息,無狀態的 jwt 是一個不錯的選擇。一旦涉及到了設備信息,就須要對 jwt 添加額外的狀態支持,增長了認證的複雜度,此時選用 session 是一個不錯的選擇。

jwt 不是萬能的,是否採用 jwt,須要根據業務需求來肯定。


我是山月,一個喜歡跑步與登山的程序員,我會按期分享全棧文章在我的公衆號中。若是你對全棧面試,前端工程化,graphql,devops,我的服務器運維以及微服務感興趣的話,能夠關注我

若是你對全棧面試,前端工程化,graphql,devops,我的服務器運維以及微服務感興趣的話,能夠關注我

相關文章
相關標籤/搜索