JWT 登錄認(rèn)證及 Token 自動(dòng)續(xù)期方案解讀
點(diǎn)擊下方“IT牧場(chǎng)”,選擇“設(shè)為星標(biāo)”

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

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

如果在JWT中存儲(chǔ)了敏感信息,可以解碼出來非常的不安全
性能
經(jīng)過編碼之后JWT將非常長(zhǎng),cookie的限制大小一般是4k,cookie很可能放不下,所以JWT一般放在local storage里面。并且用戶在系統(tǒng)中的每一次http請(qǐng)求都會(huì)把JWT攜帶在Header里面,HTTP請(qǐng)求的Header可能比Body還要大。而sessionId只是很短的一個(gè)字符串,因此使用JWT的HTTP請(qǐng)求比使用session的開銷大得多
一次性
無狀態(tài)是JWT的特點(diǎn),但也導(dǎo)致了這個(gè)問題,JWT是一次性的。想修改里面的內(nèi)容,就必須簽發(fā)一個(gè)新的JWT
無法廢棄
一旦簽發(fā)一個(gè)JWT,在到期之前就會(huì)始終有效,無法中途廢棄。若想廢棄,一種常用的處理手段是結(jié)合redis
續(xù)簽
如果使用JWT做會(huì)話管理,傳統(tǒng)的cookie續(xù)簽方案一般都是框架自帶的,session有效期30分鐘,30分鐘內(nèi)如果有訪問,有效期被刷新至30分鐘。一樣的道理,要改變JWT的有效時(shí)間,就要簽發(fā)新的JWT。
最簡(jiǎn)單的一種方式是每次請(qǐng)求刷新JWT,即每個(gè)HTTP請(qǐng)求都返回一個(gè)新的JWT。這個(gè)方法不僅暴力不優(yōu)雅,而且每次請(qǐng)求都要做JWT的加密解密,會(huì)帶來性能問題。另一種方法是在redis中單獨(dú)為每個(gè)JWT設(shè)置過期時(shí)間,每次訪問時(shí)刷新JWT的過期時(shí)間
選擇JWT或session
我投JWT一票,JWT有很多缺點(diǎn),但是在分布式環(huán)境下不需要像session一樣額外實(shí)現(xiàn)多機(jī)數(shù)據(jù)共享,雖然seesion的多機(jī)數(shù)據(jù)共享可以通過粘性session、session共享、session復(fù)制、持久化session、terracoa實(shí)現(xiàn)seesion復(fù)制等多種成熟的方案來解決這個(gè)問題。但是JWT不需要額外的工作,使用JWT不香嗎?且JWT一次性的缺點(diǎn)可以結(jié)合redis進(jìn)行彌補(bǔ)。
揚(yáng)長(zhǎng)補(bǔ)短,因此在實(shí)際項(xiàng)目中選擇的是使用JWT來進(jìn)行認(rèn)證
功能實(shí)現(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,自定義過期時(shí)間?毫秒
?????*
?????*?@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;
????????}
????}
????/**
?????*?檢驗(yàn)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中不帶有過期時(shí)間,token的過期時(shí)間由redis進(jìn)行管理 UserTokenDTO中不帶有敏感信息,如password字段不會(huì)出現(xiàn)在token中
Redis工具類
public?final?class?RedisServiceImpl?implements?RedisService?{
????/**
?????*?過期時(shí)長(zhǎng)
?????*/
????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簡(jiǎn)單封裝
業(yè)務(wù)實(shí)現(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;
}
將對(duì)應(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;
}
說明:
更新用戶密碼時(shí)需要重新生成新的token,并將新的token返回給前端,由前端更新保存在local storage中的token,同時(shí)更新存儲(chǔ)在redis中的token,這樣實(shí)現(xiàn)可以避免用戶重新登陸,用戶體驗(yàn)感不至于太差
其他說明
在實(shí)際項(xiàng)目中,用戶分為普通用戶和管理員用戶,只有管理員用戶擁有刪除用戶的權(quán)限,這一塊功能也是涉及token操作的,但是我太懶了,demo工程就不寫了
在實(shí)際項(xiàng)目中,密碼傳輸是加密過的
攔截器類
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.判斷請(qǐng)求是否有效
????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;
}
說明:
攔截器中主要做兩件事,一是對(duì)token進(jìn)行校驗(yàn),二是判斷token是否需要進(jìn)行續(xù)期
token校驗(yàn):
判斷id對(duì)應(yīng)的token是否不存在,不存在則token過期 若token存在則比較token是否一致,保證同一時(shí)間只有一個(gè)用戶操作
token自動(dòng)續(xù)期:
為了不頻繁操作redis,只有當(dāng)離過期時(shí)間只有30分鐘時(shí)才更新過期時(shí)間
攔截器配置類
@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();
????}
}干貨分享
最近將個(gè)人學(xué)習(xí)筆記整理成冊(cè),使用PDF分享。關(guān)注我,回復(fù)如下代碼,即可獲得百度盤地址,無套路領(lǐng)取!
?001:《Java并發(fā)與高并發(fā)解決方案》學(xué)習(xí)筆記;?002:《深入JVM內(nèi)核——原理、診斷與優(yōu)化》學(xué)習(xí)筆記;?003:《Java面試寶典》?004:《Docker開源書》?005:《Kubernetes開源書》?006:《DDD速成(領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)速成)》?007:全部?008:加技術(shù)群討論
加個(gè)關(guān)注不迷路
喜歡就點(diǎn)個(gè)"在看"唄^_^
