JWT 登錄認(rèn)證及 token 自動續(xù)期方案解讀
來源:juejin.cn/post/6932702419344162823
過去這段時間主要負(fù)責(zé)了項目中的用戶管理模塊,用戶管理模塊會涉及到加密及認(rèn)證流程,加密已經(jīng)在前面的文章中介紹了,可以閱讀用戶管理模塊:
https://juejin.cn/post/6916150628955717646
今天就來講講認(rèn)證功能的技術(shù)選型及實現(xiàn)。技術(shù)上沒啥難度當(dāng)然也沒啥挑戰(zhàn),但是對一個原先沒寫過認(rèn)證功能的菜雞甜來說也是一種鍛煉吧
技術(shù)選型
要實現(xiàn)認(rèn)證功能,很容易就會想到JWT或者session,但是兩者有啥區(qū)別?各自的優(yōu)缺點?應(yīng)該Pick誰?奪命三連

區(qū)別
基于session和基于JWT的方式的主要區(qū)別就是用戶的狀態(tài)保存的位置,session是保存在服務(wù)端的,而JWT是保存在客戶端的
認(rèn)證流程
基于session的認(rèn)證流程
用戶在瀏覽器中輸入用戶名和密碼,服務(wù)器通過密碼校驗后生成一個session并保存到數(shù)據(jù)庫 服務(wù)器為用戶生成一個sessionId,并將具有sesssionId的cookie放置在用戶瀏覽器中,在后續(xù)的請求中都將帶有這個cookie信息進行訪問 服務(wù)器獲取cookie,通過獲取cookie中的sessionId查找數(shù)據(jù)庫判斷當(dāng)前請求是否有效
基于JWT的認(rèn)證流程
用戶在瀏覽器中輸入用戶名和密碼,服務(wù)器通過密碼校驗后生成一個token并保存到數(shù)據(jù)庫 前端獲取到token,存儲到cookie或者local storage中,在后續(xù)的請求中都將帶有這個token信息進行訪問 服務(wù)器獲取token值,通過查找數(shù)據(jù)庫判斷當(dāng)前token是否有效
優(yōu)缺點
JWT保存在客戶端,在分布式環(huán)境下不需要做額外工作。而session因為保存在服務(wù)端,分布式環(huán)境下需要實現(xiàn)多機數(shù)據(jù)共享 session一般需要結(jié)合Cookie實現(xiàn)認(rèn)證,所以需要瀏覽器支持cookie,因此移動端無法使用session認(rèn)證方案
安全性
JWT的payload使用的是base64編碼的,因此在JWT中不能存儲敏感數(shù)據(jù)。而session的信息是存在服務(wù)端的,相對來說更安全

如果在JWT中存儲了敏感信息,可以解碼出來非常的不安全
性能
經(jīng)過編碼之后JWT將非常長,cookie的限制大小一般是4k,cookie很可能放不下,所以JWT一般放在local storage里面。并且用戶在系統(tǒng)中的每一次http請求都會把JWT攜帶在Header里面,HTTP請求的Header可能比Body還要大。而sessionId只是很短的一個字符串,因此使用JWT的HTTP請求比使用session的開銷大得多
一次性
無狀態(tài)是JWT的特點,但也導(dǎo)致了這個問題,JWT是一次性的。想修改里面的內(nèi)容,就必須簽發(fā)一個新的JWT
無法廢棄
一旦簽發(fā)一個JWT,在到期之前就會始終有效,無法中途廢棄。若想廢棄,一種常用的處理手段是結(jié)合redis
續(xù)簽
如果使用JWT做會話管理,傳統(tǒng)的cookie續(xù)簽方案一般都是框架自帶的,session有效期30分鐘,30分鐘內(nèi)如果有訪問,有效期被刷新至30分鐘。一樣的道理,要改變JWT的有效時間,就要簽發(fā)新的JWT。
最簡單的一種方式是每次請求刷新JWT,即每個HTTP請求都返回一個新的JWT。這個方法不僅暴力不優(yōu)雅,而且每次請求都要做JWT的加密解密,會帶來性能問題。另一種方法是在redis中單獨為每個JWT設(shè)置過期時間,每次訪問時刷新JWT的過期時間
選擇JWT或session
我投JWT一票,JWT有很多缺點,但是在分布式環(huán)境下不需要像session一樣額外實現(xiàn)多機數(shù)據(jù)共享,雖然seesion的多機數(shù)據(jù)共享可以通過粘性session、session共享、session復(fù)制、持久化session、terracoa實現(xiàn)seesion復(fù)制等多種成熟的方案來解決這個問題。但是JWT不需要額外的工作,使用JWT不香嗎?且JWT一次性的缺點可以結(jié)合redis進行彌補。
揚長補短,因此在實際項目中選擇的是使用JWT來進行認(rèn)證
功能實現(xiàn)
JWT所需依賴
<dependency>
????<groupId>com.auth0groupId>
????<artifactId>java-jwtartifactId>
????<version>3.10.3version>
dependency>
JWT工具類
public?class?JWTUtil?{
????private?static?final?Logger?logger?=?LoggerFactory.getLogger(JWTUtil.class);
????//私鑰
????private?static?final?String?TOKEN_SECRET?=?"123456";
????/**
?????*?生成token,自定義過期時間?毫秒
?????*
?????*?@param?userTokenDTO
?????*?@return
?????*/
????public?static?String?generateToken(UserTokenDTO?userTokenDTO)?{
????????try?{
????????????//?私鑰和加密算法
????????????Algorithm?algorithm?=?Algorithm.HMAC256(TOKEN_SECRET);
????????????//?設(shè)置頭部信息
????????????Map?header?=?new?HashMap<>(2);
????????????header.put("Type",?"Jwt");
????????????header.put("alg",?"HS256");
????????????return?JWT.create()
????????????????????.withHeader(header)
????????????????????.withClaim("token",?JSONObject.toJSONString(userTokenDTO))
????????????????????//.withExpiresAt(date)
????????????????????.sign(algorithm);
????????}?catch?(Exception?e)?{
????????????logger.error("generate?token?occur?error,?error?is:{}",?e);
????????????return?null;
????????}
????}
????/**
?????*?檢驗token是否正確
?????*
?????*?@param?token
?????*?@return
?????*/
????public?static?UserTokenDTO?parseToken(String?token)?{
????????Algorithm?algorithm?=?Algorithm.HMAC256(TOKEN_SECRET);
????????JWTVerifier?verifier?=?JWT.require(algorithm).build();
????????DecodedJWT?jwt?=?verifier.verify(token);
????????String?tokenInfo?=?jwt.getClaim("token").asString();
????????return?JSON.parseObject(tokenInfo,?UserTokenDTO.class);
????}
}
說明:
生成的token中不帶有過期時間,token的過期時間由redis進行管理 UserTokenDTO中不帶有敏感信息,如password字段不會出現(xiàn)在token中
Redis工具類
public?final?class?RedisServiceImpl?implements?RedisService?{
????/**
?????*?過期時長
?????*/
????private?final?Long?DURATION?=?1?*?24?*?60?*?60?*?1000L;
????@Resource
????private?RedisTemplate?redisTemplate;
????private?ValueOperations?valueOperations;
????@PostConstruct
????public?void?init()?{
????????RedisSerializer?redisSerializer?=?new?StringRedisSerializer();
????????redisTemplate.setKeySerializer(redisSerializer);
????????redisTemplate.setValueSerializer(redisSerializer);
????????redisTemplate.setHashKeySerializer(redisSerializer);
????????redisTemplate.setHashValueSerializer(redisSerializer);
????????valueOperations?=?redisTemplate.opsForValue();
????}
????@Override
????public?void?set(String?key,?String?value)?{
????????valueOperations.set(key,?value,?DURATION,?TimeUnit.MILLISECONDS);
????????log.info("key={},?value?is:?{}?into?redis?cache",?key,?value);
????}
????@Override
????public?String?get(String?key)?{
????????String?redisValue?=?valueOperations.get(key);
????????log.info("get?from?redis,?value?is:?{}",?redisValue);
????????return?redisValue;
????}
????@Override
????public?boolean?delete(String?key)?{
????????boolean?result?=?redisTemplate.delete(key);
????????log.info("delete?from?redis,?key?is:?{}",?key);
????????return?result;
????}
????@Override
????public?Long?getExpireTime(String?key)?{
????????return?valueOperations.getOperations().getExpire(key);
????}
}
RedisTemplate簡單封裝
業(yè)務(wù)實現(xiàn)
登陸功能
public?String?login(LoginUserVO?loginUserVO)?{
????//1.判斷用戶名密碼是否正確
????UserPO?userPO?=?userMapper.getByUsername(loginUserVO.getUsername());
????if?(userPO?==?null)?{
????????throw?new?UserException(ErrorCodeEnum.TNP1001001);
????}
????if?(!loginUserVO.getPassword().equals(userPO.getPassword()))?{
????????throw?new?UserException(ErrorCodeEnum.TNP1001002);
????}
????//2.用戶名密碼正確生成token
????UserTokenDTO?userTokenDTO?=?new?UserTokenDTO();
????PropertiesUtil.copyProperties(userTokenDTO,?loginUserVO);
????userTokenDTO.setId(userPO.getId());
????userTokenDTO.setGmtCreate(System.currentTimeMillis());
????String?token?=?JWTUtil.generateToken(userTokenDTO);
????//3.存入token至redis
????redisService.set(userPO.getId(),?token);
????return?token;
}
說明:
判斷用戶名密碼是否正確 用戶名密碼正確則生成token 將生成的token保存至redis
登出功能
public?boolean?loginOut(String?id)?{
?????boolean?result?=?redisService.delete(id);
?????if?(!redisService.delete(id))?{
????????throw?new?UserException(ErrorCodeEnum.TNP1001003);
?????}
?????return?result;
}
將對應(yīng)的key刪除即可
更新密碼功能
public?String?updatePassword(UpdatePasswordUserVO?updatePasswordUserVO)?{
????//1.修改密碼
????UserPO?userPO?=?UserPO.builder().password(updatePasswordUserVO.getPassword())
????????????.id(updatePasswordUserVO.getId())
????????????.build();
????UserPO?user?=?userMapper.getById(updatePasswordUserVO.getId());
????if?(user?==?null)?{
????????throw?new?UserException(ErrorCodeEnum.TNP1001001);
????}
????if?(userMapper.updatePassword(userPO)?!=?1)?{
????????throw?new?UserException(ErrorCodeEnum.TNP1001005);
????}
????//2.生成新的token
????UserTokenDTO?userTokenDTO?=?UserTokenDTO.builder()
????????????.id(updatePasswordUserVO.getId())
????????????.username(user.getUsername())
????????????.gmtCreate(System.currentTimeMillis()).build();
????String?token?=?JWTUtil.generateToken(userTokenDTO);
????//3.更新token
????redisService.set(user.getId(),?token);
????return?token;
}
說明:
更新用戶密碼時需要重新生成新的token,并將新的token返回給前端,由前端更新保存在local storage中的token,同時更新存儲在redis中的token,這樣實現(xiàn)可以避免用戶重新登陸,用戶體驗感不至于太差
其他說明
在實際項目中,用戶分為普通用戶和管理員用戶,只有管理員用戶擁有刪除用戶的權(quán)限,這一塊功能也是涉及token操作的,但是我太懶了,demo工程就不寫了
在實際項目中,密碼傳輸是加密過的
攔截器類
public?boolean?preHandle(HttpServletRequest?request,?HttpServletResponse?response,
?????????????????????????????Object?handler)?throws?Exception?{
????String?authToken?=?request.getHeader("Authorization");
????String?token?=?authToken.substring("Bearer".length()?+?1).trim();
????UserTokenDTO?userTokenDTO?=?JWTUtil.parseToken(token);
????//1.判斷請求是否有效
????if?(redisService.get(userTokenDTO.getId())?==?null?
????????????||?!redisService.get(userTokenDTO.getId()).equals(token))?{
????????return?false;
????}
????//2.判斷是否需要續(xù)期
????if?(redisService.getExpireTime(userTokenDTO.getId())?1?*?60?*?30)?{
????????redisService.set(userTokenDTO.getId(),?token);
????????log.error("update?token?info,?id?is:{},?user?info?is:{}",?userTokenDTO.getId(),?token);
????}
????return?true;
}
說明:
攔截器中主要做兩件事,一是對token進行校驗,二是判斷token是否需要進行續(xù)期
token校驗:
判斷id對應(yīng)的token是否不存在,不存在則token過期 若token存在則比較token是否一致,保證同一時間只有一個用戶操作
token自動續(xù)期:
為了不頻繁操作redis,只有當(dāng)離過期時間只有30分鐘時才更新過期時間
攔截器配置類
@Configuration
public?class?InterceptorConfig?implements?WebMvcConfigurer?{
????@Override
????public?void?addInterceptors(InterceptorRegistry?registry)?{
????????registry.addInterceptor(authenticateInterceptor())
????????????????.excludePathPatterns("/logout/**")
????????????????.excludePathPatterns("/login/**")
????????????????.addPathPatterns("/**");
????}
????@Bean
????public?AuthenticateInterceptor?authenticateInterceptor()?{
????????return?new?AuthenticateInterceptor();
????}
}如有文章對你有幫助,
“在看”和轉(zhuǎn)發(fā)是對我最大的支持!
推薦:
點擊領(lǐng)取:151個大廠面試講解!(圖片可上下滑動!)??


