經過前面兩篇文章《JSON Web Token - 在Web應用間安全地傳遞信息》《八幅漫畫理解使用JSON Web Token設計單點登錄系統(tǒng)》的科普,相信大家應該已經知道了 JWT 協(xié)議是什么了。至少看到
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJxaWFubWlJZCI6InFtMTAzNTNzaEQiLCJpc3MiOiJhcHBfcW0xMDM1M3NoRCIsInBsYXRmb3JtIjoiYXBwIn0.cMNwyDTFVYMLL4e7ts50GFHTvlSJLDpePtHXzu7z9j4
這樣形如 A.B.C 的字符串時能敏感地認出這是使用了 jwt。發(fā)了這兩篇文章后,有不少讀者在文末留言,表達了對 jwt 使用方式的一些疑惑,以及到底哪些場景適合使用 jwt。我并不是 jwt 方面的專家,和不少讀者一樣,起初研究時我也存在相同疑惑,甚至在逐漸接觸后產生了更大的疑惑,經過這段時間項目中的使用和一些自己思考,把個人的總結整理成此文。
編碼,簽名,加密
這些基礎知識簡單地介紹下,千萬別搞混了三個概念。在 jwt 中恰好同時涉及了這三個概念,筆者用大白話來做下通俗的講解(非嚴謹定義,供個人理解)
編碼(encode)和解碼(decode)
一般是編碼解碼是為了方便以字節(jié)的方式表示數(shù)據(jù),便于存儲和網(wǎng)絡傳輸。整個 jwt 串會被置于 http 的 Header 或者 url 中,為了不出現(xiàn)亂碼解析錯誤等意外,編碼是有必要的。在 jwt 中以.分割的三個部分都經過 base64 編碼(secret 部分是否進行 base64 編碼是可選的,header 和 payload 則是必須進行 base64 編碼)。注意,編碼的一個特點:編碼和解碼的整個過程是可逆的。得知編碼方式后,整個 jwt 串便是明文了,隨意找個網(wǎng)站驗證下解碼后的內容:

所以注意一點,payload 是一定不能夠攜帶敏感數(shù)據(jù)如密碼等信息的。
簽名(signature)
簽名的目的主要是為了驗證我是“我”。jwt 中常用的簽名算法是 HS256,可能大多數(shù)人對這個簽名算法不熟悉,但 md5,sha 這樣的簽名算法肯定是為人熟知的,簽名算法共同的特點是整個過程是不可逆的。由于簽名之前的主體內容(header,payload)會攜帶在 jwt 字符串中,所以需要使用帶有密鑰(yuè)的簽名算法,密鑰是服務器和簽發(fā)者共享的。header 部分和 payload 部分如果被篡改,由于篡改者不知道密鑰是什么,也無法生成新的 signature 部分,服務端也就無法通過,在 jwt 中,消息體是透明的,使用簽名可以保證消息不被篡改。
前面轉載的文章中,原作者將 HS256 稱之為加密算法,不太嚴謹。
加密(encryption)
加密是將明文信息改變?yōu)殡y以讀取的密文內容,使之不可讀。只有擁有解密方法的對象,經由解密過程,才能將密文還原為正常可讀的內容。加密算法通常按照加密方式的不同分為對稱加密(如 AES)和非對稱加密(如 RSA)。你可能會疑惑:“jwt 中哪兒涉及加密算法了?”,其實 jwt 的 第一部分(header) 中的 alg 參數(shù)便可以指定不同的算法來生成第三部分(signature),大部分支持 jwt 的框架至少都內置 rsa 這種非對稱加密方式。這里誕生了第一個疑問
疑問:一提到 rsa,大多數(shù)人第一想到的是非對稱加密算法,而 jwt 的第三部分明確的英文定義是 signature,這不是矛盾嗎?
劃重點!
rsa 加密和 rsa 簽名 是兩個概念!(嚇得我都換行了)
這兩個用法很好理解:
-
既然是加密,自然是不希望別人知道我的消息,只有我自己才能解密,所以公鑰負責加密,私鑰負責解密。這是大多數(shù)的使用場景,使用 rsa 來加密。
-
既然是簽名,自然是希望別人不能冒充我發(fā)消息,只有我才能發(fā)布簽名,所以私鑰負責簽名,公鑰負責驗證。
所以,在客戶端使用 rsa 算法生成 jwt 串時,是使用私鑰來“加密”的,而公鑰是公開的,誰都可以解密,內容也無法變更(篡改者無法得知私鑰)。
所以,在 jwt 中并沒有純粹的加密過程,而是使加密之虛,行簽名之實。
什么場景該適合使用jwt?
來聊聊幾個場景,注意,以下的幾個場景不是都和jwt貼合。
-
一次性驗證
比如用戶注冊后需要發(fā)一封郵件讓其激活賬戶,通常郵件中需要有一個鏈接,這個鏈接需要具備以下的特性:能夠標識用戶,該鏈接具有時效性(通常只允許幾小時之內激活),不能被篡改以激活其他可能的賬戶…這種場景就和 jwt 的特性非常貼近,jwt 的 payload 中固定的參數(shù):iss 簽發(fā)者和 exp 過期時間正是為其做準備的。
-
restful api 的無狀態(tài)認證
使用 jwt 來做 restful api 的身份認證也是值得推崇的一種使用方案。客戶端和服務端共享 secret;過期時間由服務端校驗,客戶端定時刷新;簽名信息不可被修改…spring security oauth jwt 提供了一套完整的 jwt 認證體系,以筆者的經驗來看:使用 oauth2 或 jwt 來做 restful api 的認證都沒有大問題,oauth2 功能更多,支持的場景更豐富,后者實現(xiàn)簡單。
-
使用 jwt 做單點登錄+會話管理(不推薦)
在《八幅漫畫理解使用JSON Web Token設計單點登錄系統(tǒng)》一文中提及了使用 jwt 來完成單點登錄,本文接下來的內容主要就是圍繞這一點來進行討論。如果你正在考慮使用 jwt+cookie 代替 session+cookie ,我強力不推薦你這么做。
首先明確一點:使用 jwt 來設計單點登錄系統(tǒng)是一個不太嚴謹?shù)恼f法。首先 cookie+jwt 的方案前提是非跨域的單點登錄(cookie 無法被自動攜帶至其他域名),其次單點登錄系統(tǒng)包含了很多技術細節(jié),至少包含了身份認證和會話管理,這還不涉及到權限管理。如果覺得比較抽象,不妨用傳統(tǒng)的 session+cookie 單點登錄方案來做類比,通常我們可以選擇 spring security(身份認證和權限管理的安全框架)和 spring session(session 共享)來構建,而選擇用 jwt 設計單點登錄系統(tǒng)需要解決很多傳統(tǒng)方案中同樣存在和本不存在的問題,以下一一詳細羅列。
jwt token泄露了怎么辦?
前面的文章下有不少人留言提到這個問題,我則認為這不是問題。傳統(tǒng)的 session+cookie 方案,如果泄露了 sessionId,別人同樣可以盜用你的身份。揚湯止沸不如釜底抽薪,不妨來追根溯源一下,什么場景會導致你的 jwt 泄露。
遵循如下的實踐可以盡可能保護你的 jwt 不被泄露:使用 https 加密你的應用,返回 jwt 給客戶端時設置 httpOnly=true 并且使用 cookie 而不是 LocalStorage 存儲 jwt,這樣可以防止 XSS 攻擊和 CSRF 攻擊(對這兩種攻擊感興趣的童鞋可以看下 spring security 中對他們的介紹CSRF,XSS)
你要是正在使用 jwt 訪問一個接口,這個時候你的同事跑過來把你的 jwt 抄走了,這種泄露,恕在下無力
secret如何設計
jwt 唯一存儲在服務端的只有一個 secret,個人認為這個 secret 應該設計成和用戶相關的屬性,而不是一個所有用戶公用的統(tǒng)一值。這樣可以有效的避免一些注銷和修改密碼時遇到的窘境。
注銷和修改密碼
傳統(tǒng)的 session+cookie 方案用戶點擊注銷,服務端清空 session 即可,因為狀態(tài)保存在服務端。但 jwt 的方案就比較難辦了,因為 jwt 是無狀態(tài)的,服務端通過計算來校驗有效性。沒有存儲起來,所以即使客戶端刪除了 jwt,但是該 jwt 還是在有效期內,只不過處于一個游離狀態(tài)。分析下痛點:注銷變得復雜的原因在于 jwt 的無狀態(tài)。我提供幾個方案,視具體的業(yè)務來決定能不能接受。
-
僅僅清空客戶端的 cookie,這樣用戶訪問時就不會攜帶 jwt,服務端就認為用戶需要重新登錄。這是一個典型的假注銷,對于用戶表現(xiàn)出退出的行為,實際上這個時候攜帶對應的 jwt 依舊可以訪問系統(tǒng)。
-
清空或修改服務端的用戶對應的 secret,這樣在用戶注銷后,jwt 本身不變,但是由于 secret 不存在或改變,則無法完成校驗。這也是為什么將 secret 設計成和用戶相關的原因。
-
借助第三方存儲自己管理 jwt 的狀態(tài),可以以 jwt 為 key,實現(xiàn)去 redis 一類的緩存中間件中去校驗存在性。方案設計并不難,但是引入 redis 之后,就把無狀態(tài)的 jwt 硬生生變成了有狀態(tài)了,違背了 jwt 的初衷。實際上這個方案和 session 都差不多了。
修改密碼則略微有些不同,假設號被到了,修改密碼(是用戶密碼,不是 jwt 的 secret)之后,盜號者在原 jwt 有效期之內依舊可以繼續(xù)訪問系統(tǒng),所以僅僅清空 cookie 自然是不夠的,這時,需要強制性的修改 secret。在我的實踐中就是這樣做的。
續(xù)簽問題
續(xù)簽問題可以說是我抵制使用 jwt 來代替?zhèn)鹘y(tǒng) session 的最大原因,因為 jwt 的設計中我就沒有發(fā)現(xiàn)它將續(xù)簽認為是自身的一個特性。傳統(tǒng)的 cookie 續(xù)簽方案一般都是框架自帶的,session 有效期 30 分鐘,30 分鐘內如果有訪問,session 有效期被刷新至 30 分鐘。而 jwt 本身的 payload 之中也有一個 exp 過期時間參數(shù),來代表一個 jwt 的時效性,而 jwt 想延期這個 exp 就有點身不由己了,因為 payload 是參與簽名的,一旦過期時間被修改,整個 jwt 串就變了,jwt 的特性天然不支持續(xù)簽!
如果你一定要使用 jwt 做會話管理(payload 中存儲會話信息),也不是沒有解決方案,但個人認為都不是很令人滿意
-
每次請求刷新 jwt
jwt 修改 payload 中的 exp 后整個 jwt 串就會發(fā)生改變,那…就讓它變好了,每次請求都返回一個新的 jwt 給客戶端。太暴力了,不用我贅述這樣做是多么的不優(yōu)雅,以及帶來的性能問題。
但,至少這是最簡單的解決方案。
-
只要快要過期的時候刷新 jwt
一個上述方案的改造點是,只在最后的幾分鐘返回給客戶端一個新的 jwt。這樣做,觸發(fā)刷新 jwt 基本就要看運氣了,如果用戶恰巧在最后幾分鐘訪問了服務器,觸發(fā)了刷新,萬事大吉;如果用戶連續(xù)操作了 27 分鐘,只有最后的 3 分鐘沒有操作,導致未刷新 jwt,無疑會令用戶抓狂。
-
完善 refreshToken
借鑒 oauth2 的設計,返回給客戶端一個 refreshToken,允許客戶端主動刷新 jwt。一般而言,jwt 的過期時間可以設置為數(shù)小時,而 refreshToken 的過期時間設置為數(shù)天。
我認為該方案并可行性是存在的,但是為了解決 jwt 的續(xù)簽把整個流程改變了,為什么不考慮下 oauth2 的 password 模式和 client 模式呢?
-
使用 redis 記錄獨立的過期時間
實際上我的項目中由于歷史遺留問題,就是使用 jwt 來做登錄和會話管理的,為了解決續(xù)簽問題,我們在 redis 中單獨會每個 jwt 設置了過期時間,每次訪問時刷新 jwt 的過期時間,若 jwt 不存在與 redis 中則認為過期。
tips:精確控制 redis 的過期時間不是件容易的事,可以參考我最近的一篇借助于 spring session 講解 redis 過期時間的排坑記錄。
同樣改變了 jwt 的流程,不過嘛,世間安得兩全法。我只能奉勸各位還未使用 jwt 做會話管理的朋友,盡量還是選用傳統(tǒng)的 session+cookie 方案,有很多成熟的分布式 session 框架和安全框架供你開箱即用。
jwt,oauth2,session千絲萬縷的聯(lián)系
具體的對比不在此文介紹,就一位讀者的留言回復下它的提問
這么長一個字符串,還不如我把數(shù)據(jù)存到數(shù)據(jù)庫,給一個長的很難碰撞的key來映射,也就是專用token。
這位兄弟認為 jwt 太長了,是不是可以考慮使用和 oauth2 一樣的 uuid 來映射。這里面自然是有問題的,jwt 不僅僅是作為身份的認證(驗證簽名是否正確,簽發(fā)者是否存在,有限期是否過期),還在其 payload 中存儲著會話信息,這是 jwt 和 session 的最大區(qū)別,一個在客戶端攜帶會話信息,一個在服務端存儲會話信息。如果真的是要將 jwt 的信息置于在共享存儲中,那再找不到任何使用 jwt 的意義了。
jwt 和 oauth2 都可以用于 restful 的認證,就我個人的使用經驗來看,spring security oauth2 可以很好的使用多種認證模式:client 模式,password 模式,implicit 模式(authorization code 模式不算單純的接口認證模式),也可以很方便的實現(xiàn)權限控制,什么樣的 api 需要什么樣的權限,什么樣的資源需要什么樣的 scope…而 jwt 我只用它來實現(xiàn)過身份認證,功能較為單一(可能是我沒發(fā)現(xiàn)更多用法)。
總結
在 web 應用中,使用 jwt 代替 session 存在不小的風險,你至少得解決本文中提及的那些問題,絕大多數(shù)情況下,傳統(tǒng)的 cookie-session 機制工作得更好。jwt 適合做簡單的 restful api 認證,頒發(fā)一個固定有效期的 jwt,降低 jwt 暴露的風險,不要對 jwt 做服務端的狀態(tài)管理,這樣才能體現(xiàn)出 jwt 無狀態(tài)的優(yōu)勢。
可能對 jwt 的使用場景還有一些地方未被我察覺,后續(xù)會研究下 spring security oauth jwt 的源碼,不知到時會不會有新發(fā)現(xiàn)。
免責聲明:本文內容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!