一、問題背景
最近搭建springcloud的項目,項目采取了Jwt + spring security 來進行登錄驗證,Jwt token 鎖定用戶的失效時間,但是由於 jwt token特性導致token失效時間無法刷新,所以必須新創建一個token令牌,用來代替之前已失效token。
(token失效時間無法刷新的原因是由於jwt創建token是根據jwt保存的相關信息來計算的,過期時間是其中的一個計算維度,所以一旦過期時間改了,那么生成的token值也就變了。)
之后為了解決這個問題,結合了redis,將token值保存到redis中,用戶操作后刷新redis的有效時間,這樣如果jwt token失效了,再檢查 redis 中保存token的key是否失效,如果沒有失效,那么就重新創建jwt token ,失效了,就重新登錄。
保存在redis中的 key 是用戶名, 但是我需要把 jwt token 轉化后從 claims 中取出這個用戶名,一開始我直接轉化,進行debug的時候發現如果token超時了,jwt 沒有返回轉化結果, 而是直接拋出了異常,我查看JWT所有的轉化方法,發現Jwt所有的轉化最終處理都是parse(claimJws)這個方法,而這個方法正是我一開始用的解析方法。
原本是調用jwtUtil(jwt的工具類),傳入一個token,判斷是否過期,然而卻莫名其妙得拋異常了,而業務中還需要根據是否過期進行后續邏輯!異常如下:
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2020-07-29T14:48:14Z. Current time: 2020-07-29T14:48:50Z, a difference of 36843 milliseconds. Allowed clock skew: 0 milliseconds. at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:385) at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:481) at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:541) at com.smart.util.JwtUtil.parseJwt(JwtUtil.java:63) at com.smart.util.JwtUtil.isTokenExpired(JwtUtil.java:93)
二、問題原因
根據報錯堆棧信息找到了DefaultJwtParser類中,找到了問題的原因。
boolean allowSkew = this.allowedClockSkewMillis > 0L; if (claims != null) { Date now = this.clock.now(); long nowTime = now.getTime(); Date exp = claims.getExpiration(); String nbfVal; SimpleDateFormat sdf; if (exp != null) { long maxTime = nowTime - this.allowedClockSkewMillis; Date max = allowSkew ? new Date(maxTime) : now; if (max.after(exp)) { sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); String expVal = sdf.format(exp); nbfVal = sdf.format(now); long differenceMillis = maxTime - exp.getTime(); String msg = "JWT expired at " + expVal + ". Current time: " + nbfVal + ", a difference of " + differenceMillis + " milliseconds. Allowed clock skew: " + this.allowedClockSkewMillis + " milliseconds."; throw new ExpiredJwtException((Header)header, claims, msg); } } ...... }
看到結尾的throw new ExpiredJwtException,我相信就找到了問題的關鍵,原來在在解析token並發現這個token已經過期了,它作出的反應是直接拋異常。
異常定義的構造方法中除了msg信息,還有claims和header信息。
檢查claims發現,在異常之前token其實已經解析完畢。
這樣也就代表着,拋出的這個異常 ExpiredJwtException 中有一個參數 claims 就是解析后的token,那么本次這個問題也就解決了。
catch ExpiredJwtException 異常后,直接從異常中獲取解析的數據即可,如下介紹。
三、過期報錯了,如何進行后續業務處理
回到我們的工具類中的解析jwt的方法:
public Claims parseJwt(String token){ Claims claims = Jwts.parser() .setSigningKey(signKey) // 設置標識名
.parseClaimsJws(token) //解析token
.getBody(); return claims; }
改為:不管是否過期都返回 claims 對象
//不管是否過期,都返回claims對象
public Claims parseJwt(String token){ Claims claims; try { claims = Jwts.parser() .setSigningKey(signKey) // 設置標識名
.parseClaimsJws(token) //解析token
.getBody(); } catch (ExpiredJwtException e) { claims = e.getClaims(); } return claims; }
可以從異常中找到這個過期的claim對象信息;判斷token是否過期的方法我也相應修改了,如下:
public Boolean isTokenExpired(String token) { //不管是否過期,都返回claims對象
Claims claims = this.parseJwt(token); Date expiration = claims.getExpiration(); //和當前時間進行對比來判斷是否過期
return new Date(System.currentTimeMillis()).after(expiration); }
參考文章:
https://blog.csdn.net/qq_38294335/article/details/107669630
https://blog.csdn.net/Piteover/article/details/90676279