<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          JWT 登錄認(rèn)證及 token 自動(dòng)續(xù)期方案解讀

          共 7181字,需瀏覽 15分鐘

           ·

          2022-05-23 22:46

          程序員的成長(zhǎng)之路
          互聯(lián)網(wǎng)/程序員/技術(shù)/資料共享?
          關(guān)注


          閱讀本文大概需要 6 分鐘。

          來(lái)自:juejin.cn/post/6932702419344162823

          過(guò)去這段時(shí)間主要負(fù)責(zé)了項(xiàng)目中的用戶管理模塊,用戶管理模塊會(huì)涉及到加密及認(rèn)證流程,加密已經(jīng)在前面的文章中介紹了,可以閱讀用戶管理模塊:
          https://juejin.cn/post/6916150628955717646
          今天就來(lái)講講認(rèn)證功能的技術(shù)選型及實(shí)現(xiàn)。技術(shù)上沒(méi)啥難度當(dāng)然也沒(méi)啥挑戰(zhàn),但是對(duì)一個(gè)原先沒(méi)寫過(guò)認(rèn)證功能的菜雞甜來(lái)說(shuō)也是一種鍛煉吧

          技術(shù)選型

          要實(shí)現(xiàn)認(rèn)證功能,很容易就會(huì)想到JWT或者session,但是兩者有啥區(qū)別?各自的優(yōu)缺點(diǎn)?應(yīng)該P(yáng)ick誰(shuí)?奪命三連

          區(qū)別

          基于session和基于JWT的方式的主要區(qū)別就是用戶的狀態(tài)保存的位置,session是保存在服務(wù)端的,而JWT是保存在客戶端的

          認(rèn)證流程

          基于session的認(rèn)證流程
          • 用戶在瀏覽器中輸入用戶名和密碼,服務(wù)器通過(guò)密碼校驗(yàn)后生成一個(gè)session并保存到數(shù)據(jù)庫(kù)
          • 服務(wù)器為用戶生成一個(gè)sessionId,并將具有sesssionId的cookie放置在用戶瀏覽器中,在后續(xù)的請(qǐng)求中都將帶有這個(gè)cookie信息進(jìn)行訪問(wèn)
          • 服務(wù)器獲取cookie,通過(guò)獲取cookie中的sessionId查找數(shù)據(jù)庫(kù)判斷當(dāng)前請(qǐng)求是否有效
          基于JWT的認(rèn)證流程
          • 用戶在瀏覽器中輸入用戶名和密碼,服務(wù)器通過(guò)密碼校驗(yàn)后生成一個(gè)token并保存到數(shù)據(jù)庫(kù)
          • 前端獲取到token,存儲(chǔ)到cookie或者local storage中,在后續(xù)的請(qǐng)求中都將帶有這個(gè)token信息進(jìn)行訪問(wèn)
          • 服務(wù)器獲取token值,通過(guò)查找數(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)端無(wú)法使用session認(rèn)證方案

          安全性

          JWT的payload使用的是base64編碼的,因此在JWT中不能存儲(chǔ)敏感數(shù)據(jù)。而session的信息是存在服務(wù)端的,相對(duì)來(lái)說(shuō)更安全
          如果在JWT中存儲(chǔ)了敏感信息,可以解碼出來(lái)非常的不安全

          性能

          經(jīng)過(guò)編碼之后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的開(kāi)銷大得多

          一次性

          無(wú)狀態(tài)是JWT的特點(diǎn),但也導(dǎo)致了這個(gè)問(wèn)題,JWT是一次性的。想修改里面的內(nèi)容,就必須簽發(fā)一個(gè)新的JWT

          無(wú)法廢棄

          一旦簽發(fā)一個(gè)JWT,在到期之前就會(huì)始終有效,無(wú)法中途廢棄。若想廢棄,一種常用的處理手段是結(jié)合redis

          續(xù)簽

          如果使用JWT做會(huì)話管理,傳統(tǒng)的cookie續(xù)簽方案一般都是框架自帶的,session有效期30分鐘,30分鐘內(nèi)如果有訪問(wèn),有效期被刷新至30分鐘。一樣的道理,要改變JWT的有效時(shí)間,就要簽發(fā)新的JWT。
          最簡(jiǎn)單的一種方式是每次請(qǐng)求刷新JWT,即每個(gè)HTTP請(qǐng)求都返回一個(gè)新的JWT。這個(gè)方法不僅暴力不優(yōu)雅,而且每次請(qǐng)求都要做JWT的加密解密,會(huì)帶來(lái)性能問(wèn)題。另一種方法是在redis中單獨(dú)為每個(gè)JWT設(shè)置過(guò)期時(shí)間,每次訪問(wèn)時(shí)刷新JWT的過(guò)期時(shí)間

          選擇JWT或session

          我投JWT一票,JWT有很多缺點(diǎn),但是在分布式環(huán)境下不需要像session一樣額外實(shí)現(xiàn)多機(jī)數(shù)據(jù)共享,雖然seesion的多機(jī)數(shù)據(jù)共享可以通過(guò)粘性session、session共享、session復(fù)制、持久化session、terracoa實(shí)現(xiàn)seesion復(fù)制等多種成熟的方案來(lái)解決這個(gè)問(wèn)題。但是JWT不需要額外的工作,使用JWT不香嗎?且JWT一次性的缺點(diǎn)可以結(jié)合redis進(jìn)行彌補(bǔ)。
          揚(yáng)長(zhǎng)補(bǔ)短,因此在實(shí)際項(xiàng)目中選擇的是使用JWT來(lái)進(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,自定義過(guò)期時(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);
          ????}
          }
          說(shuō)明:
          • 生成的token中不帶有過(guò)期時(shí)間,token的過(guò)期時(shí)間由redis進(jìn)行管理
          • UserTokenDTO中不帶有敏感信息,如password字段不會(huì)出現(xiàn)在token中

          Redis工具類

          public?final?class?RedisServiceImpl?implements?RedisService?{
          ????/**
          ?????*?過(guò)期時(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;
          }
          說(shuō)明:
          • 判斷用戶名密碼是否正確
          • 用戶名密碼正確則生成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;
          }
          說(shuō)明:
          更新用戶密碼時(shí)需要重新生成新的token,并將新的token返回給前端,由前端更新保存在local storage中的token,同時(shí)更新存儲(chǔ)在redis中的token,這樣實(shí)現(xiàn)可以避免用戶重新登陸,用戶體驗(yàn)感不至于太差
          其他說(shuō)明
          在實(shí)際項(xiàng)目中,用戶分為普通用戶和管理員用戶,只有管理員用戶擁有刪除用戶的權(quán)限,這一塊功能也是涉及token操作的,但是我太懶了,demo工程就不寫了
          在實(shí)際項(xiàng)目中,密碼傳輸是加密過(guò)的
          攔截器類
          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;
          }
          說(shuō)明:
          攔截器中主要做兩件事,一是對(duì)token進(jìn)行校驗(yàn),二是判斷token是否需要進(jìn)行續(xù)期
          token校驗(yàn):
          • 判斷id對(duì)應(yīng)的token是否不存在,不存在則token過(guò)期
          • 若token存在則比較token是否一致,保證同一時(shí)間只有一個(gè)用戶操作
          token自動(dòng)續(xù)期:
          為了不頻繁操作redis,只有當(dāng)離過(guò)期時(shí)間只有30分鐘時(shí)才更新過(guò)期時(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();
          ????}
          }

          推薦閱讀:

          SpringBoot+Querydsl 框架,大大簡(jiǎn)化復(fù)雜查詢操作

          數(shù)據(jù)庫(kù)連接池設(shè)置多大才合適?

          互聯(lián)網(wǎng)初中高級(jí)大廠面試題(9個(gè)G)

          內(nèi)容包含Java基礎(chǔ)、JavaWeb、MySQL性能優(yōu)化、JVM、鎖、百萬(wàn)并發(fā)、消息隊(duì)列、高性能緩存、反射、Spring全家桶原理、微服務(wù)、Zookeeper......等技術(shù)棧!

          ?戳閱讀原文領(lǐng)取!? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??朕已閱?

          瀏覽 37
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  wwwwxxxx黄色在线观看 | 五月天激情丁香 | 综合一和综合二图片小说 | 情侣操逼视频 | 亚洲伊人成人网站 |