天天看點

jwt單點登入_深入了解 JWT 的使用場景和優劣

(給ImportNew加星标,提高Java技能)

轉自:Kirito的技術分享/作者:徐靖峰

到底哪些場景适合使用 jwt?我并不是 jwt 方面的專家,和不少讀者一樣,起初研究時我也存在相同疑惑,甚至在逐漸接觸後産生了更大的疑惑,經過這段時間項目中的使用和一些自己思考,把個人的總結整理成此文。

編碼,簽名,加密

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

編碼(encode)和解碼(decode)

一般是編碼解碼是為了友善以位元組的方式表示資料,便于存儲和網絡傳輸。整個 jwt 串會被置于 http 的 Header 或者 url 中,為了不出現亂碼解析錯誤等意外,編碼是有必要的。在 jwt 中以 . 分割的三個部分都經過 base64 編碼(secret 部分是否進行 base64 編碼是可選的,header 和 payload 則是必須進行 base64 編碼)。注意,編碼的一個特點:編碼和解碼的整個過程是可逆的。得知編碼方式後,整個 jwt 串便是明文了,随意找個網站驗證下解碼後的内容:

jwt單點登入_深入了解 JWT 的使用場景和優劣

base64

是以注意一點,payload 是一定不能夠攜帶敏感資料如密碼等資訊的。

簽名(signature)

簽名的目的主要是為了驗證我是“我”。jwt 中常用的簽名算法是 HS256,可能大多數人對這個簽名算法不熟悉,但 md5,sha 這樣的簽名算法肯定是為人熟知的,簽名算法共同的特點是整個過程是不可逆的。由于簽名之前的主體内容(header,payload)會攜帶在 jwt 字元串中,是以需要使用帶有密鑰(yuè)的簽名算法,密鑰是伺服器和簽發者共享的。header 部分和 payload 部分如果被篡改,由于篡改者不知道密鑰是什麼,也無法生成新的 signature 部分,服務端也就無法通過,在 jwt 中,消息體是透明的,使用簽名可以保證消息不被篡改。

加密(encryption)

加密是将明文資訊改變為難以讀取的密文内容,使之不可讀。隻有擁有解密方法的對象,經由解密過程,才能将密文還原為正常可讀的内容。加密算法通常按照加密方式的不同分為對稱加密(如 AES)和非對稱加密(如 RSA)。你可能會疑惑:“jwt 中哪兒涉及加密算法了?”,其實 jwt 的 第一部分(header) 中的 alg 參數便可以指定不同的算法來生成第三部分(signature),大部分支援 jwt 的架構至少都内置 rsa 這種非對稱加密方式。這裡誕生了第一個疑問

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

劃重點!

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

這兩個用法很好了解:

  • 既然是加密,自然是不希望别人知道我的消息,隻有我自己才能解密,是以公鑰負責加密,私鑰負責解密。這是大多數的使用場景,使用 rsa 來加密。
  • 既然是簽名,自然是希望别人不能冒充我發消息,隻有我才能釋出簽名,是以私鑰負責簽名,公鑰負責驗證。

是以,在用戶端使用 rsa 算法生成 jwt 串時,是使用私鑰來“加密”的,而公鑰是公開的,誰都可以解密,内容也無法變更(篡改者無法得知私鑰)。

是以,在 jwt 中并沒有純粹的加密過程,而是使加密之虛,行簽名之實。

什麼場景該适合使用jwt?

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

1.一次性驗證

比如使用者注冊後需要發一封郵件讓其激活賬戶,通常郵件中需要有一個連結,這個連結需要具備以下的特性:能夠辨別使用者,該連結具有時效性(通常隻允許幾小時之内激活),不能被篡改以激活其他可能的賬戶…這種場景就和 jwt 的特性非常貼近,jwt 的 payload 中固定的參數:iss 簽發者和 exp 過期時間正是為其做準備的。

2.restful api 的無狀态認證

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

3.使用 jwt 做單點登入+會話管理(不推薦)

在《八幅漫畫了解使用JSON Web Token設計單點登入系統》一文中提及了使用 jwt 來完成單點登入,本文接下來的内容主要就是圍繞這一點來進行讨論。如果你正在考慮使用 jwt+cookie 代替 session+cookie ,我強力不推薦你這麼做。

首先明确一點:使用 jwt 來設計單點登入系統是一個不太嚴謹的說法。首先 cookie+jwt 的方案前提是非跨域的單點登入(cookie 無法被自動攜帶至其他域名),其次單點登入系統包含了很多技術細節,至少包含了身份認證和會話管理,這還不涉及到權限管理。如果覺得比較抽象,不妨用傳統的 session+cookie 單點登入方案來做類比,通常我們可以選擇 spring security(身份認證和權限管理的安全架構)和 spring session(session 共享)來建構,而選擇用 jwt 設計單點登入系統需要解決很多傳統方案中同樣存在和本不存在的問題,以下一一詳細羅列。

jwt token洩露了怎麼辦?

前面的文章下有不少人留言提到這個問題,我則認為這不是問題。傳統的 session+cookie 方案,如果洩露了 sessionId,别人同樣可以盜用你的身份。揚湯止沸不如釜底抽薪,不妨來追根溯源一下,什麼場景會導緻你的 jwt 洩露。

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

你要是正在使用 jwt 通路一個接口,這個時候你的同僚跑過來把你的 jwt 抄走了,這種洩露,恕在下無力

secret如何設計

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 給用戶端。太暴力了,不用我贅述這樣做是多麼的不優雅,以及帶來的性能問題。

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

2.隻要快要過期的時候重新整理 jwt

一個上述方案的改造點是,隻在最後的幾分鐘傳回給用戶端一個新的 jwt。這樣做,觸發重新整理 jwt 基本就要看運氣了,如果使用者恰巧在最後幾分鐘通路了伺服器,觸發了重新整理,萬事大吉;如果使用者連續操作了 27 分鐘,隻有最後的 3 分鐘沒有操作,導緻未重新整理 jwt,無疑會令使用者抓狂。

3.完善 refreshToken

借鑒 oauth2 的設計,傳回給用戶端一個 refreshToken,允許用戶端主動重新整理 jwt。一般而言,jwt 的過期時間可以設定為數小時,而 refreshToken 的過期時間設定為數天。

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

4.使用 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 的源碼,不知到時會不會有新發現。

推薦閱讀

(點選标題可跳轉閱讀)

Java 10 中的局部類型推斷,好像并沒什麼用!

細思極恐 - 你真的會寫 Java 嗎?

你的也是我的。3 例 ko 多線程,局部變量透傳

看完本文有收獲?請轉發分享給更多人

關注「ImportNew」,提升Java技能

jwt單點登入_深入了解 JWT 的使用場景和優劣

好文章,我在看❤️