深刻理解JWT的使用場景和優劣

前面簡單介紹了JWT的基礎只是和簡單的小Demo,可是對於JWT的應用場景和優缺點掌握的還夠,這些東西只有本身實踐過才能搞清楚其中的細節。在網上看到一個大佬對這塊講的比較好,就轉載過來一塊兒學習下。 
原文連接

這樣形如 A.B.C 的字符串時能敏感地認出這是使用了 jwt。發了這兩篇文章後,有很多讀者在文末留言,表達了對 jwt 使用方式的一些疑惑,以及到底哪些場景適合使用 jwt。我並非 jwt 方面的專家,和很多讀者同樣,起初研究時我也存在相同疑惑,甚至在逐漸接觸後產生了更大的疑惑,通過這段時間項目中的使用和一些本身思考,把我的的總結整理成此文。web

這些基礎知識簡單地介紹下,千萬別搞混了三個概念。在 jwt 中剛好同時涉及了這三個概念,筆者用大白話來作下通俗的講解(非嚴謹定義,供我的理解)redis

編碼(encode)和解碼(decode)

通常是編碼解碼是爲了方便以字節的方式表示數據,便於存儲和網絡傳輸。整個 jwt 串會被置於 http 的 Header 或者 url 中,爲了避免出現亂碼解析錯誤等意外,編碼是有必要的。在 jwt 中以 . 分割的三個部分都通過 base64 編碼(secret 部分是否進行 base64 編碼是可選的,header 和 payload 則是必須進行 base64 編碼)。注意,編碼的一個特色:編碼和解碼的整個過程是可逆的。得知編碼方式後,整個 jwt 串即是明文了,隨意找個網站驗證下解碼後的內容:算法

 base64spring

因此注意一點,payload 是必定不可以攜帶敏感數據如密碼等信息的數據庫

簽名(signature)

簽名的目的主要是爲了驗證我是「我」。jwt 中經常使用的簽名算法是 HS256,可能大多數人對這個簽名算法不熟悉,但 md5,sha 這樣的簽名算法確定是爲人熟知的,簽名算法共同的特色是整個過程是不可逆的。因爲簽名以前的主體內容(header,payload)會攜帶在 jwt 字符串中,因此須要使用帶有密鑰(yuè)的簽名算法,密鑰是服務器和簽發者共享的。header 部分和 payload 部分若是被篡改,因爲篡改者不知道密鑰是什麼,也沒法生成新的 signature 部分,服務端也就沒法經過,在 jwt 中,消息體是透明的,使用簽名能夠保證消息不被篡改。api

前面轉載的文章中,原做者將 HS256 稱之爲加密算法,不太嚴謹。跨域

加密(encryption)

加密是將明文信息改變爲難以讀取的密文內容,使之不可讀。只有擁有解密方法的對象,經由解密過程,才能將密文還原爲正常可讀的內容。加密算法一般按照加密方式的不一樣分爲對稱加密(如 AES)和非對稱加密(如 RSA)。你可能會疑惑:「jwt 中哪兒涉及加密算法了?」,其實 jwt 的 第一部分(header) 中的 alg 參數即可以指定不一樣的算法來生成第三部分(signature),大部分支持 jwt 的框架至少都內置 rsa 這種非對稱加密方式。這裏誕生了第一個疑問緩存

疑問:一提到 rsa,大多數人第一想到的是非對稱加密算法,而 jwt 的第三部分明確的英文定義是 signature,這不是矛盾嗎?安全

劃重點!服務器

rsa 加密和 rsa 簽名 是兩個概念!(嚇得我都換行了)

這兩個用法很好理解:

  • 既然是加密,天然是不但願別人知道個人消息,只有我本身才能解密,因此公鑰負責加密,私鑰負責解密。這是大多數的使用場景,使用 rsa 來加密。

  • 既然是簽名,天然是但願別人不能冒充我發消息,只有我才能發佈簽名,因此私鑰負責簽名,公鑰負責驗證

因此,在客戶端使用 rsa 算法生成 jwt 串時,是使用私鑰來「加密」的,而公鑰是公開的,誰均可以解密,內容也沒法變動(篡改者沒法得知私鑰)。

因此,在 jwt 中並無純粹的加密過程,而是使加密之虛,行簽名之實。

 

來聊聊幾個場景,注意,如下的幾個場景不是都和jwt貼合。

  1. 一次性驗證

好比用戶註冊後須要發一封郵件讓其激活帳戶,一般郵件中須要有一個連接,這個連接須要具有如下的特性:可以標識用戶,該連接具備時效性(一般只容許幾小時以內激活),不能被篡改以激活其餘可能的帳戶…這種場景就和 jwt 的特性很是貼近,jwt 的 payload 中固定的參數:iss 簽發者和 exp 過時時間正是爲其作準備的。

  1. restful api 的無狀態認證

使用 jwt 來作 restful api 的身份認證也是值得推崇的一種使用方案。客戶端和服務端共享 secret;過時時間由服務端校驗,客戶端定時刷新;簽名信息不可被修改…spring security oauth jwt 提供了一套完整的 jwt 認證體系,以筆者的經驗來看:使用 oauth2 或 jwt 來作 restful api 的認證都沒有大問題,oauth2 功能更多,支持的場景更豐富,後者實現簡單。

  1. 使用 jwt 作單點登陸+會話管理(不推薦)

在《八幅漫畫理解使用JSON Web Token設計單點登陸系統》一文中說起了使用 jwt 來完成單點登陸,本文接下來的內容主要就是圍繞這一點來進行討論。若是你正在考慮使用 jwt+cookie 代替 session+cookie ,我強力不推薦你這麼作。

首先明確一點:使用 jwt 來設計單點登陸系統是一個不太嚴謹的說法。首先 cookie+jwt 的方案前提是非跨域的單點登陸(cookie 沒法被自動攜帶至其餘域名),其次單點登陸系統包含了不少技術細節,至少包含了身份認證和會話管理,這還不涉及到權限管理。若是以爲比較抽象,不妨用傳統的 session+cookie 單點登陸方案來作類比,一般咱們能夠選擇 spring security(身份認證和權限管理的安全框架)和 spring session(session 共享)來構建,而選擇用 jwt 設計單點登陸系統須要解決不少傳統方案中一樣存在和本不存在的問題,如下一一詳細羅列。

 

前面的文章下有很多人留言提到這個問題,我則認爲這不是問題。傳統的 session+cookie 方案,若是泄露了 sessionId,別人一樣能夠盜用你的身份。揚湯止沸不如釜底抽薪,不妨來追根溯源一下,什麼場景會致使你的 jwt 泄露。

遵循以下的實踐能夠儘量保護你的 jwt 不被泄露:使用 https 加密你的應用,返回 jwt 給客戶端時設置 httpOnly=true 而且使用 cookie 而不是 LocalStorage 存儲 jwt,這樣能夠防止 XSS 攻擊和 CSRF 攻擊(對這兩種攻擊感興趣的童鞋能夠看下 spring security 中對他們的介紹CSRF,XSS)

你要是正在使用 jwt 訪問一個接口,這個時候你的同事跑過來把你的 jwt 抄走了,這種泄露,恕在下無力

jwt 惟一存儲在服務端的只有一個 secret,我的認爲這個 secret 應該設計成和用戶相關的屬性,而不是一個全部用戶公用的統一值。這樣能夠有效的避免一些註銷和修改密碼時遇到的窘境。

 

傳統的 session+cookie 方案用戶點擊註銷,服務端清空 session 便可,由於狀態保存在服務端。但 jwt 的方案就比較難辦了,由於 jwt 是無狀態的,服務端經過計算來校驗有效性。沒有存儲起來,因此即便客戶端刪除了 jwt,可是該 jwt 仍是在有效期內,只不過處於一個遊離狀態。分析下痛點:註銷變得複雜的緣由在於 jwt 的無狀態。我提供幾個方案,視具體的業務來決定能不能接受。

  • 僅僅清空客戶端的 cookie,這樣用戶訪問時就不會攜帶 jwt,服務端就認爲用戶須要從新登陸。這是一個典型的假註銷,對於用戶表現出退出的行爲,實際上這個時候攜帶對應的 jwt 依舊能夠訪問系統。

  • 清空或修改服務端的用戶對應的 secret,這樣在用戶註銷後,jwt 自己不變,可是因爲 secret 不存在或改變,則沒法完成校驗。這也是爲何將 secret 設計成和用戶相關的緣由。

  • 藉助第三方存儲本身管理 jwt 的狀態,能夠以 jwt 爲 key,實現去 redis 一類的緩存中間件中去校驗存在性。方案設計並不難,可是引入 redis 以後,就把無狀態的 jwt 硬生生變成了有狀態了,違背了 jwt 的初衷。實際上這個方案和 session 都差很少了。

修改密碼則略微有些不一樣,假設號被到了,修改密碼(是用戶密碼,不是 jwt 的 secret)以後,盜號者在原 jwt 有效期以內依舊能夠繼續訪問系統,因此僅僅清空 cookie 天然是不夠的,這時,須要強制性的修改 secret。在個人實踐中就是這樣作的。

續簽問題能夠說是我抵制使用 jwt 來代替傳統 session 的最大緣由,由於 jwt 的設計中我就沒有發現它將續簽認爲是自身的一個特性。傳統的 cookie 續簽方案通常都是框架自帶的,session 有效期 30 分鐘,30 分鐘內若是有訪問,session 有效期被刷新至 30 分鐘。而 jwt 自己的 payload 之中也有一個 exp 過時時間參數,來表明一個 jwt 的時效性,而 jwt 想延期這個 exp 就有點身不禁己了,由於 payload 是參與簽名的,一旦過時時間被修改,整個 jwt 串就變了,jwt 的特性自然不支持續簽!

若是你必定要使用 jwt 作會話管理(payload 中存儲會話信息),也不是沒有解決方案,但我的認爲都不是很使人滿意

  1. 每次請求刷新 jwt

jwt 修改 payload 中的 exp 後整個 jwt 串就會發生改變,那…就讓它變好了,每次請求都返回一個新的 jwt 給客戶端。太暴力了,不用我贅述這樣作是多麼的不優雅,以及帶來的性能問題。

但,至少這是最簡單的解決方案。

  1. 只要快要過時的時候刷新 jwt

一個上述方案的改造點是,只在最後的幾分鐘返回給客戶端一個新的 jwt。這樣作,觸發刷新 jwt 基本就要看運氣了,若是用戶恰巧在最後幾分鐘訪問了服務器,觸發了刷新,萬事大吉;若是用戶連續操做了 27 分鐘,只有最後的 3 分鐘沒有操做,致使未刷新 jwt,無疑會令用戶抓狂。

  1. 完善 refreshToken

借鑑 oauth2 的設計,返回給客戶端一個 refreshToken,容許客戶端主動刷新 jwt。通常而言,jwt 的過時時間能夠設置爲數小時,而 refreshToken 的過時時間設置爲數天。

我認爲該方案並可行性是存在的,可是爲了解決 jwt 的續簽把整個流程改變了,爲何不考慮下 oauth2 的 password 模式和 client 模式呢?

  1. 使用 redis 記錄獨立的過時時間

實際上個人項目中因爲歷史遺留問題,就是使用 jwt 來作登陸和會話管理的,爲了解決續簽問題,咱們在 redis 中單獨會每一個 jwt 設置了過時時間,每次訪問時刷新 jwt 的過時時間,若 jwt 不存在與 redis 中則認爲過時。

tips:精確控制 redis 的過時時間不是件容易的事,能夠參考我最近的一篇藉助於 spring session 講解 redis 過時時間的排坑記錄。

一樣改變了 jwt 的流程,不過嘛,世間安得兩全法。我只能奉勸各位還未使用 jwt 作會話管理的朋友,儘可能仍是選用傳統的 session+cookie 方案,有不少成熟的分佈式 session 框架和安全框架供你開箱即用。

jwt,oauth2,session千絲萬縷的聯繫

具體的對比不在此文介紹,就一位讀者的留言回覆下它的提問

這麼長一個字符串,還不如我把數據存到數據庫,給一個長的很難碰撞的key來映射,也就是專用token。

這位兄弟認爲 jwt 太長了,是否是能夠考慮使用和 oauth2 同樣的 uuid 來映射。這裏面天然是有問題的,jwt 不只僅是做爲身份的認證(驗證簽名是否正確,簽發者是否存在,有限期是否過時),還在其 payload 中存儲着會話信息,這是 jwt 和 session 的最大區別,一個在客戶端攜帶會話信息,一個在服務端存儲會話信息。若是真的是要將 jwt 的信息置於在共享存儲中,那再找不到任何使用 jwt 的意義了。

jwt 和 oauth2 均可以用於 restful 的認證,就我我的的使用經驗來看,spring security oauth2 能夠很好的使用多種認證模式:client 模式,password 模式,implicit 模式(authorization code 模式不算單純的接口認證模式),也能夠很方便的實現權限控制,什麼樣的 api 須要什麼樣的權限,什麼樣的資源須要什麼樣的 scope…而 jwt 我只用它來實現過身份認證,功能較爲單一(多是我沒發現更多用法)。

總結

在 web 應用中,使用 jwt 代替 session 存在不小的風險,你至少得解決本文中說起的那些問題,絕大多數狀況下,傳統的 cookie-session 機制工做得更好。jwt 適合作簡單的 restful api 認證,頒發一個固定有效期的 jwt,下降 jwt 暴露的風險,不要對 jwt 作服務端的狀態管理,這樣才能體現出 jwt 無狀態的優點。

可能對 jwt 的使用場景還有一些地方未被我察覺,後續會研究下 spring security oauth jwt 的源碼,不知到時會不會有新發現。

 

我的以爲大佬寫的很不錯,有興趣的話能夠去關注一下,或點擊上方的連接。

相關文章
相關標籤/搜索