1. 程式人生 > >理解JWT的使用場景和優劣

理解JWT的使用場景和優劣

class=“line”>eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxaWFubWlJZCI6InFtMTAzNTNzaEQiLCJpc3MiOiJhcHBfcW0xMDM1M3NoRCIsInBsYXRmb3JtIjoiYXBwIn0.cMNwyDTFVYMLL4e7ts50GFHTvlSJLDpePtHXzu7z9j4

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

編碼,簽名,加密

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

編碼(encode)和解碼(decode)

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

base64base64

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

簽名(signature)

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

前面轉載的文章中,原作者將 HS256 稱之為加密演算法,不太嚴謹。

加密(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 過期時間正是為其做準備的。

  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 設計單點登入系統需要解決很多傳統方案中同樣存在和本不存在的問題,以下一一詳細羅列。

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 中儲存會話資訊),也不是沒有解決方案,但個人認為都不是很令人滿意

  • 每次請求重新整理 jwt

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

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

  • 只要快要過期的時候重新整理 jwt

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

  • 完善 refreshToken

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

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

  • 使用 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 的原始碼,不知到時會不會有新發現。

原文:http://blog.didispace.com/learn-how-to-use-jwt-xjf/