關(guān)于JWT Token 自動(dòng)續(xù)期的解決方案

前言
在前后端分離的開發(fā)模式下,前端用戶登錄成功后后端服務(wù)會(huì)給用戶頒發(fā)一個(gè)jwt token。前端(如vue)在接收到j(luò)wt token后會(huì)將token存儲(chǔ)到LocalStorage中。
后續(xù)每次請(qǐng)求都會(huì)將此token放在請(qǐng)求頭中傳遞到后端服務(wù),后端服務(wù)會(huì)有一個(gè)過濾器對(duì)token進(jìn)行攔截校驗(yàn),校驗(yàn)token是否過期,如果token過期則會(huì)讓前端跳轉(zhuǎn)到登錄頁面重新登錄。
因?yàn)閖wt token中一般會(huì)包含用戶的基本信息,為了保證token的安全性,一般會(huì)將token的過期時(shí)間設(shè)置的比較短。
但是這樣又會(huì)導(dǎo)致前端用戶需要頻繁登錄(token過期),甚至有的表單比較復(fù)雜,前端用戶在填寫表單時(shí)需要思考較長時(shí)間,等真正提交表單時(shí)后端校驗(yàn)發(fā)現(xiàn)token過期失效了不得不跳轉(zhuǎn)到登錄頁面。
如果真發(fā)生了這種情況前端用戶肯定是要罵人的,用戶體驗(yàn)非常不友好。本篇內(nèi)容就是在前端用戶無感知的情況下實(shí)現(xiàn)token的自動(dòng)續(xù)期,避免頻繁登錄、表單填寫內(nèi)容丟失情況的發(fā)生。
實(shí)現(xiàn)原理
jwt token自動(dòng)續(xù)期的實(shí)現(xiàn)原理如下:
登錄成功后將用戶生成的 jwt token作為key、value存儲(chǔ)到cache緩存里面 (這時(shí)候key、value值一樣),將緩存有效期設(shè)置為 token有效時(shí)間的2倍。當(dāng)該用戶再次請(qǐng)求時(shí),通過后端的一個(gè) jwt Filter校驗(yàn)前端token是否是有效token,如果前端token無效表明是非法請(qǐng)求,直接拋出異常即可;根據(jù)規(guī)則取出cache token,判斷cache token是否存在,此時(shí)主要分以下幾種情況: cache token 不存在
這種情況表明該用戶賬戶空閑超時(shí),返回用戶信息已失效,請(qǐng)重新登錄。cache token 存在,則需要使用jwt工具類驗(yàn)證該cache token 是否過期超時(shí),不過期無需處理。
過期則表示該用戶一直在操作只是token失效了,后端程序會(huì)給token對(duì)應(yīng)的key映射的value值重新生成jwt token并覆蓋value值,該緩存生命周期重新計(jì)算。
實(shí)現(xiàn)邏輯的核心原理:前端請(qǐng)求Header中設(shè)置的token保持不變,校驗(yàn)有效性以緩存中的token為準(zhǔn)。
代碼實(shí)現(xiàn)(偽碼)
登錄成功后給用戶簽發(fā)token,并設(shè)置token的有效期
...
SysUser?sysUser?=?userService.getUser(username,password);
if(null?!==?sysUser){
????String?token?=?JwtUtil.sign(sysUser.getUsername(),?
sysUser.getPassword());
}
...
public?static?String?sign(String?username,?String?secret)?{
????//設(shè)置token有效期為30分鐘
?Date?date?=?new?Date(System.currentTimeMillis()?+?30?*?60?*?1000);
?//使用HS256生成token,密鑰則是用戶的密碼
?Algorithm?algorithm?=?Algorithm.HMAC256(secret);
?//?附帶username信息
?return?JWT.create().withClaim("username",?username).withExpiresAt(date).sign(algorithm);
}
將token存入redis,并設(shè)定過期時(shí)間,將redis的過期時(shí)間設(shè)置成token過期時(shí)間的兩倍
Sting?tokenKey?=?"sys:user:token"?+?token;
redisUtil.set(tokenKey,?token);
redisUtil.expire(tokenKey,?30?*?60?*?2);
過濾器校驗(yàn)token,校驗(yàn)token有效性
public?void?doFilter(ServletRequest?req,?ServletResponse?res,?FilterChain?chain)?throws?IOException,?ServletException?{
????//從header中獲取token
?String?token?=?httpServletRequest.getHeader("token")
?if(null?==?token){
??throw?new?RuntimeException("illegal?request,token?is?necessary!")
?}
????//解析token獲取用戶名
?String?username?=?JwtUtil.getUsername(token);
?//根據(jù)用戶名獲取用戶實(shí)體,在實(shí)際開發(fā)中從redis取
?User?user?=?userService.findByUser(username);
????if(null?==?user){
??throw?new?RuntimeException("illegal?request,token?is?Invalid!")
????}
?//校驗(yàn)token是否失效,自動(dòng)續(xù)期
?if(!refreshToken(token,username,user.getPassword())){
??throw?new?RuntimeException("illegal?request,token?is?expired!")
?}
?...
}
實(shí)現(xiàn)token的自動(dòng)續(xù)期
public?boolean?refreshToken(String?token,?String?userName,?String?passWord)?{
?Sting?tokenKey?=?"sys:user:token"?+?token?;
?String?cacheToken?=?String.valueOf(redisUtil.get(tokenKey));
?if?(StringUtils.isNotEmpty(cacheToken))?{
??//?校驗(yàn)token有效性,注意需要校驗(yàn)的是緩存中的token
??if?(!JwtUtil.verify(cacheToken,?userName,?passWord))?{
???String?newToken?=?JwtUtil.sign(userName,?passWord);
???//?設(shè)置超時(shí)時(shí)間
???redisUtil.set(tokenKey,?newToken)?;
???redisUtil.expire(tokenKey,?30?*?60?*?2);
??}
??return?true;
?}
?return?false;
}
...
public?static?boolean?verify(String?token,?String?username,?String?secret)?{
?try?{
??//?根據(jù)密碼生成JWT效驗(yàn)器
??Algorithm?algorithm?=?Algorithm.HMAC256(secret);
??JWTVerifier?verifier?=?JWT.require(algorithm).withClaim("username",?username).build();
??//?效驗(yàn)TOKEN
??DecodedJWT?jwt?=?verifier.verify(token);
??return?true;
?}?catch?(Exception?exception)?{
??return?false;
?}
}
本文中jwt的相關(guān)操作是基于 com.auth0.java-jwt 實(shí)現(xiàn),大家可以通過閱讀原文獲取 JwtUtil 工具類。
小結(jié)
jwt token實(shí)現(xiàn)邏輯的核心原理是 前端請(qǐng)求Header中設(shè)置的token保持不變,校驗(yàn)有效性以緩存中的token為準(zhǔn),千萬不要直接校驗(yàn)Header中的token。實(shí)現(xiàn)原理部分大家好好體會(huì)一下,思路比實(shí)現(xiàn)更重要!
