jwt 實踐以及與 session 對比

shanyue發表於2018-07-21

JSON Web Token 是 rfc7519 出的一份標準,使用 JSON 來傳遞資料,用於判定使用者是否登入狀態。

jwt 之前,使用 session 來做使用者認證。

以下程式碼均使用 javascript 編寫。

原文連結見 山月的部落格

session

傳統登入的方式是使用 session + token

token 是指在客戶端使用 token 作為使用者狀態憑證,瀏覽器一般儲存在 localStorage 或者 cookie 中。

session 是指在伺服器端使用 redis 或者 sql 類資料庫,儲存 user_id 以及 token 的鍵值對關係,基本工作原理如下。

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

// 通過 token 獲取 user_id, 完成認證過程
function getUserIdByToken (token) {
  return sessions[token]
}
複製程式碼

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

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

如果不使用 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 不需要加密,只需要進行簽名,保證不被篡改

這便是 jwt 的思想,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

Sign 由 HeaderPayload 以及 secretOrPrivateKey 計算而成。

對於 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({ userId: 10085 }, secrect + 'ACDE', { expiresIn: 60 * 10 })
複製程式碼

郵箱校驗

現在網站在註冊成功後會進行郵箱校驗,具體做法是給郵箱發一個連結,使用者點開連結校驗成功。

// 把郵箱以及使用者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 實現

當使用者登出時,如何使該 token 失效

因為 jwt 無狀態,不儲存使用者裝置資訊,沒法單純使用它完成以上問題,可以再利用資料庫儲存一些狀態完成。

  • session: 只需要把 user_id 對應的 token 清掉即可
  • jwt: 使用 redis,維護一張黑名單,使用者登出時加入黑名單(簽名),過期時間與 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,需要根據業務需求來確定。

相關文章