一文理解JWT鑒權(quán)登錄的應用
如果對cookie/token有疑問的,可以查看之前的博客快速了解會話管理三劍客cookie、session和JWT
Json Web Token (JWT)是為在網(wǎng)絡應用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標準。JWT被設計為緊湊且安全,特別適用于分布式站點的單點登錄(SSO)場景。JWT一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便于從資源服務器獲取資源,也可以增加一些額外的其它業(yè)務邏輯所必須的聲明信息。
本文將針對JWT在身份驗證業(yè)務場景下的應用進行講解。
前置知識
JWT的數(shù)據(jù)結(jié)構(gòu)
JWT的表現(xiàn)形式是個字符串,它由頭部、載荷與簽名這三部分組成,中間以「.」分隔。像下面這樣:

頭部Header
頭部幫助應用程序定義如何處理接收到的令牌。頭部信息以JSON格式顯示,轉(zhuǎn)化為JWT時需要用base64url算法進行編碼。
{
"alg": "HS256",
"typ": "JWT"
}
typ:令牌類型
alg:用于生成簽名的算法
載荷Payload
載荷用來存儲傳遞的數(shù)據(jù),比如用戶信息的姓名、性別、年齡等。載荷信息以JSON格式顯示,轉(zhuǎn)化為JWT時需要用base64url算法進行編碼。要注意的是機密信息不要放到這里,比如密碼等。
{
"name": "全菜工程師小輝",
"introduce": "啥都不會"
}
JWT規(guī)定了7個默認字段供開發(fā)者選用。
iss (issuer):簽發(fā)人
exp (expiration time):過期時間
sub (subject):主題
aud (audience):受眾,相當于接受者
nbf (Not Before):生效的起始時間
iat (Issued At):簽發(fā)時間
jti (JWT ID):編號,唯一標識
簽名Signature
對于每種加密算法,簽名都對應的一個計算公式。例如SHA256加密算法的簽名如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload) + "." +
Secret
)
當網(wǎng)關或者服務收到JWT時會計算簽名的值,并將其與接收到的簽名進行對比。如果不相同,則意味著該令牌已被不可信的一方修改或生成。
Secret(秘鑰)是一定不可以不能泄露。對于非對稱加密和對稱加密,秘鑰的形式是不同的,安全性也不一樣,但并不一定對稱加密就不好。有關這個問題的討論,之后的博客再詳細講解。
注:驗證JWT可以使用參考文檔2的網(wǎng)站。
對稱加密與非對稱加密
對稱加密是最快速、最簡單的一種加密方式,加密與解密用的是同樣的密鑰。
非對稱加密可以在不直接傳遞密鑰的情況下完成解密。這能夠確保信息的安全性,避免了直接傳遞密鑰所造成的被破解的風險。是由一對密鑰來進行加解密的過程,分別稱為公鑰和私鑰。公鑰和私鑰是成對的,可以互相解密。
加密與簽名的區(qū)別
非對稱加密中:
公鑰加密,私鑰解密:可以實現(xiàn)消息加密,防止信息被泄露。這樣只有持有對應私鑰的服務才能將消息明文解析。
私鑰加密,公鑰解密:可以實現(xiàn)數(shù)字簽名,防止信息被篡改。這樣可以確實是誰發(fā)來的消息。因為服務端的公鑰只能解對應方的私鑰加密的簽名信息。(簽名信息可以是摘要未加密信息中的一部分信息,例如JWT中的簽名)
對稱加密中,加解密使用同一個密鑰,如果秘鑰泄露,會發(fā)生極大的危險且很難察覺。
對稱加密中,簽名和驗簽使用同一個密鑰,也就意味著驗簽者既可以驗簽,也能對數(shù)據(jù)進行重新簽名、偽造簽名,不能解決造假問題。而非對稱算法很好地解決這個問題,簽名和驗簽使用不同的密鑰,避免造假問題發(fā)生。
JWT在鑒權(quán)登錄中的應用
單JWT在鑒權(quán)登錄中的使用方法
單JWT的會話管理流程如下:
在用戶登錄網(wǎng)站的時候,輸入密碼、短信驗證或者其他授權(quán)方式登錄,登錄請求到達服務端的時候,服務端對信息進行驗證,然后計算出包含用戶鑒權(quán)信息的JWT字符串作為accesstoken,返回給客戶端。
客戶端拿到accesstoken后,存儲到cookie或者瀏覽器的LocalStorage中。
客戶端再次發(fā)送非匿名的接口請求,需要在HTTP請求頭中加入accesstoken。
服務端拿到accesstoken后,驗證JWT的信息是否被篡改。

對稱加密與非對稱加密在登錄鑒權(quán)場景的區(qū)別
對稱加密:

對稱加密的秘鑰為了安全,只放在授權(quán)中心,從而導致下游微服務鑒權(quán)必須要重復請求授權(quán)中心。
一種可行的解決方法是在授權(quán)中心首次鑒權(quán)通過后,將驗證通過的信息存放到header中進行路由傳遞。但這種解決方法會受到架構(gòu)和部門協(xié)作的影響,不推薦大項目這樣做。
另一種可行的解決方法是將授權(quán)中心的鑒權(quán)功能做成工具包,開放給所有服務引入使用。但這種解決方法會存在秘鑰更迭或者泄露的問題,需要基于現(xiàn)有架構(gòu)進行優(yōu)化。
非對稱加密:

私鑰僅保存在授權(quán)中心,減少秘鑰泄露的可能;下游服務可以使用公鑰獲取JWT信息,不需要頻繁與授權(quán)中心進行通信,提高了系統(tǒng)的運作效率。
JWT在登錄鑒權(quán)場景的優(yōu)點
嚴格的結(jié)構(gòu)化。JWT載荷部分包含了與用戶相關的驗證消息,如用戶可訪問路由、訪問有效期等信息,服務器無需再去連接數(shù)據(jù)庫驗證信息的有效性,并且載荷部分支持業(yè)務的定制化。
支持跨域驗證,可以應用于單點登錄;不依賴cookie,使得其可以防止CSRF攻擊,也能在禁用 cookie 的瀏覽器環(huán)境中正常運行。
體積小,因而傳輸速度快。
傳輸方式多樣,可以通過URL/POST參數(shù)/HTTP頭部等方式傳輸。
注:實測在Amazon上4c8g的云服務上,從token模式轉(zhuǎn)換成JWT模式,注冊qps提升4倍且未遇到性能瓶頸。
單JWT在鑒權(quán)登錄中存在的問題
為了用戶體驗,accesstoken會設置較長時間,但是JWT形式的accesstoken包含了與用戶相關的驗證消息,通常情況下是不會被服務端保存,這就導致一個嚴重的問題當客戶端重置密碼后或用戶被封禁的時候,無法阻攔用戶的請求。
JWT登錄鑒權(quán)增加refreshtoken機制(雙JWT機制)來解決這個問題。
JWT登錄鑒權(quán)增加refreshtoken機制
refreshtoken是OAuth2認證中的一個概念,一般稱為“更新令牌”,和OAuth2的accesstoken同時生成。作用是用來獲取新的accesstoken,不用于接口請求的身份認證。
通常情況下,refreshtoken的有效期會比較長,而accesstoken的有效期比較短。當accesstoken由于過期而失效時,使用refreshtoken就可以獲取到新的accesstoken,如果refreshtoken失效了,用戶就只能重新登錄(但在某些業(yè)務場景,業(yè)務方想要自動續(xù)期。下一節(jié)針對這個問題有思考)。
引入refreshtoken后,會話管理流程改進如下:
客戶端輸入密碼、短信驗證或者其他授權(quán)方式登錄,登錄請求到達服務端的時候,服務端生成有效時間較短的accesstoken(例如2小時)和有效時間較長的refreshtoken(例如 30天)
客戶端拿到accesstoken和refreshtoken后,存儲到cookie或者瀏覽器的LocalStorage中。
客戶端再次發(fā)送非匿名的接口請求,需要在HTTP請求頭中加入accesstoken。如果accesstoken沒有過期,服務端鑒權(quán)后返回給客戶端需要的數(shù)據(jù)。
如果攜帶accesstoken訪問需要認證的接口時鑒權(quán)失敗,則客戶端使用refreshtoken向刷新接口申請新的accesstoken;如果refreshtoken沒有過期,服務端向客戶端下發(fā)新的 accesstoken??蛻舳耸褂眯碌腶ccesstoken重試之前鑒權(quán)失敗的接口,做到用戶對續(xù)期無感知;如果refreshtoken鑒權(quán)失敗,則客戶端跳轉(zhuǎn)至登錄界面,引導用戶重新登錄。
refreshtoken獲取流程:

refreshtoken使用流程:

雙JWT下如何進行權(quán)限管理
在用戶登錄時,將生成的refreshtoken和用戶信息進行保存。當用戶被封禁時,直接將用戶信息或者對應的refreshtoken加入黑名單。
黑名單在刷新接口的時候進行校驗,從而實現(xiàn)了雙JWT場景下的權(quán)限管理。
有人可能會覺得加在網(wǎng)關層會更好。但如果黑名單加在網(wǎng)關層的話,就失去了JWT使用的初衷,將JWT模式變成了token模式,所以不提倡在網(wǎng)關層加黑名單。
由于客戶端無法獲取到新的accesstoken,從而再也無法訪問需要認證的接口。這樣的方式雖然會有一定的窗口期(取決于accesstoken的失效時間),但基本上可以適應常規(guī)情況下對用戶登錄鑒權(quán)的精度要求。
refreshtoken的自動續(xù)期
在某些業(yè)務場景,業(yè)務方想要用戶鑒權(quán)自動續(xù)期(即用戶長期不需要手動登錄或者永久不需要手動登錄直到手動取消授權(quán))。
這里給出可行的方案,但實際上都是有需要規(guī)避的安全風險。
在refreshtoken過期之前更換新的refreshtoken。將refreshtoken過期時間設置為7天,并在每次用戶打開應用程序并每隔一定時間(例如1小時)刷新令牌。如果用戶超過7天沒有打開過應用程序,那用戶就需要再次登錄。
refreshtoken永遠不會過期。這樣的機制會導致JWT失去了意義。為了防止客戶端更換或注銷,需要以某種方式對JWT進行識別,應用程序需要提供注銷的方法。例如使用設備的名稱例如“xiaohui的iPad”來標記對應的JWT,然后用戶可以去應用程序撤銷訪問“xiaohui的iPad”,從而注銷掉refreshtoken。
JWT實例代碼
參考文檔2的網(wǎng)站列出了各種語言對應的JWT庫。
由于Auth0提供的JWT庫簡單實用,小輝項目中使用Auth0實現(xiàn)JWT功能。
Auth0的代碼見參考文檔1。
引入Auth0只需要在pom.xml文件中增加如下代碼:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.9.0</version>
</dependency>
以下為小輝項目中,脫敏后的簡化代碼,讀者可以參考下。
private static String jwtSecret = "s222dad@@13fhu123129=1232!!!3PPPdsadsashdhbn@@!!sdauS";
private static String jwtIssuer = "xiaohui";
/**
* 對稱加密算法,HMAC256創(chuàng)建JWT
*/
public static String creatJWT(){
String token = null;
try {
Algorithm algorithm = Algorithm.HMAC256(jwtSecret);
token = JWT.create()
.withIssuedAt(new Date())
.withExpiresAt(DateUtils.addHours(new Date(), 2))
.withIssuer(jwtIssuer)
.withNotBefore(new Date())
.sign(algorithm);
} catch (JWTCreationException e) {
log.error("creatJWT error {}", e);
}
return token;
}
/**
* 檢驗JWT
* @param token
* @return
*/
public static boolean checkJWT(String token) {
DecodedJWT decodedJWT = null;
try {
Algorithm algorithm = Algorithm.HMAC256(jwtSecret);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(jwtIssuer)
.build();
decodedJWT = verifier.verify(token);
} catch (TokenExpiredException e) {
log.info("checkJWT timeout,token is {},for more information {}", token, e);
return false;
} catch (JWTVerificationException e) {
log.info("checkJWT error,token is {},for more information {}", token, e);
return false;
}
return true;
}
項目也有非對稱加密算法RSA256的解決方案,不過綜合考慮項目架構(gòu)、工期與安全性等因素,最后小輝在生產(chǎn)項目使用的是HS256的對稱加密算法。
JWT需要添加一些與業(yè)務相關的參數(shù)用于檢驗,可以有效提高接口被爬的門檻和提高服務的安全性。更多有關JWT安全問題,之后的博客再詳細講解。
參考文檔:
https://github.com/auth0/java-jwt
https://jwt.io/
