<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 實現(xiàn)登錄認證 + Token 自動續(xù)期方案,這才是正確的使用姿勢!

          共 14877字,需瀏覽 30分鐘

           ·

          2022-07-31 15:12

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


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

          作者:何甜甜在嗎
          鏈接:https://juejin.cn/post/6932702419344162823

          ?
          過去這段時間主要負責了項目中的用戶管理模塊,用戶管理模塊會涉及到加密及認證流程。
          今天就來講講認證功能的技術(shù)選型及實現(xiàn)。技術(shù)上沒啥難度當然也沒啥挑戰(zhàn),但是對一個原先沒寫過認證功能的菜雞甜來說也是一種鍛煉吧

          技術(shù)選型

          要實現(xiàn)認證功能,很容易就會想到JWT或者session,但是兩者有啥區(qū)別?各自的優(yōu)缺點?應該Pick誰?奪命三連

          區(qū)別

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

          認證流程

          基于session的認證流程
          基于JWT的認證流程

          優(yōu)缺點

          安全性
          • JWT的payload使用的是base64編碼的,因此在JWT中不能存儲敏感數(shù)據(jù)。而session的信息是存在服務端的,相對來說更安全
          如果在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的特點,但也導致了這個問題,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復制、持久化sessionterracoa實現(xiàn)seesion復制等多種成熟的方案來解決這個問題。
          但是JWT不需要額外的工作,使用JWT不香嗎?且JWT一次性的缺點可以結(jié)合redis進行彌補。揚長補短,因此在實際項目中選擇的是使用JWT來進行認證。

          功能實現(xiàn)

          JWT所需依賴

          <dependency>
              <groupId>com.auth0</groupId>
              <artifactId>java-jwt</artifactId>
              <version>3.10.3</version>
          </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<String, Object> 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<String, String> 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è)務實現(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;
          }

          將對應的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對應的token是否不存在,不存在則token過期
          • 若token存在則比較token是否一致,保證同一時間只有一個用戶操作
          token自動續(xù)期: 為了不頻繁操作redis,只有當離過期時間只有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();
              }
          }

          寫在最后

          若有紕漏不足,歡迎指出。
          <END>

          推薦閱讀:

          7行代碼讓B站崩潰3小時,竟因“一個詭計多端的0”

          Spring Security 實現(xiàn)動態(tài)權(quán)限菜單方案(附源碼)

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

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

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

          瀏覽 58
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  亚洲中文字慕AV无码精品 | 国产一区在线视频 | 精品韩无码台湾成人在线 | 操逼电影网址 | 射久久久久久 |