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