JSON Web Token 是 rfc7519 出的一份標準,使用 JSON 來傳遞數據,用於斷定用戶是否登陸狀態。javascript
jwt 以前,使用 session
來作用戶認證。html
如下代碼均使用 javascript 編寫。
傳統判斷是否登陸的方式是使用 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
的登陸方案。其實存儲在 cookie
,localStorage
甚至 IndexedDB
或者 WebSQL
各有利弊,核心思想一致。github
關於 cookie
以及 token
優缺點,在 token authetication vs cookies 中有討論。web
若是不使用 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 來判斷簽名是否一致。
jwt 根據 Header
,Payload
以及 Signature
三個部分由 .
拼接而成。
Header 由非對稱加密算法和類型組成,以下
const header = { // 加密算法 alg: 'HS256', type: 'jwt' }
Payload 中由 Registered Claim 以及須要通訊的數據組成。這些數據字段也叫 Claim
。
Registered Claim
中比較重要的是 "exp" Claim
表示過時時間,在用戶登陸時會設置過時時間。
const payload = { // 表示 jwt 建立時間 iat: 1532135735, // 表示 jwt 過時時間 exp: 1532136735, // 用戶 id,用以通訊 user_id: 10086 }
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}`
關於無狀態和有狀態,在其它技術方向也有對比,好比 React 的 stateLess component
以及 stateful component
,函數式編程中的反作用能夠理解爲狀態,http 也是一個無狀態協議,須要靠 header 以及 cookie 攜帶狀態。
在用戶認證這裏,有無狀態是指是否依賴外部數據存儲,如 mysql,redis 等。
思考如下幾個關於登陸的問題如何使用 session 以及 jwt 實現,來更加清楚 jwt
的使用場景
由於 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,須要根據業務需求來肯定。
我是山月,一個喜歡跑步與登山的程序員,我會按期分享全棧文章在我的公衆號中。若是你對全棧面試,前端工程化,graphql,devops,我的服務器運維以及微服務感興趣的話,能夠關注我