相关推荐recommended
Spring Boot 使用 Hutool-jwt 实现 token 验证
作者:mmseoamin日期:2024-02-02

Spring Boot 使用 Hutool-jwt 实现 token 验证

文章目录

  • Spring Boot 使用 Hutool-jwt 实现 token 验证
  • 一、JWT 概述
    • 1、简介
    • 2、结构
    • 3、使用
    • 二、基本使用
      • 0、整体思路
      • 1、JWT 工具类
      • 2、在拦截器中校验和解析 token
      • 3、在注册和登录时签发 token
      • 4、用户信息 UserHolder

        一、JWT 概述

        1、简介

        简单地说,JWT 就是一种网络身份认证和信息交换格式。

        2、结构

        • Header 头部信息,主要声明了 JWT 的签名算法等信息;
        • Payload 载荷信息,主要承载了各种声明并传递明文数据;
        • Signature 签名,拥有该部分的 JWT 被称为 JWS,也就是签了名的 JWS ,用于校验数据
          # 整体结构
          header.payload.signature
          

          3、使用

          JWT模块的核心主要是两个类:

          1. JWT类用于链式生成、解析或验证JWT信息。
          2. JWTUtil类主要是JWT的一些工具封装,提供更加简洁的JWT生成、解析和验证工作。

          二、基本使用

          逻辑较为简单,下面的代码作为参考。

          0、整体思路

          1. 写一个工具类封装生成、校验和解析 token 的方法;
          2. 在注册和登录时生成 token ,生成的 token 存入 redis ,下次登录去 redis 获取,如果存在则直接返回,反之重新生成,并存入 redis;
          3. 在拦截器中校验和解析 token ,拿到 token 中有用的信息存入 private static final ThreadLocal *THREAD_LOCAL* = new ThreadLocal<>(); ,以便后续取用。

          1、JWT 工具类

          根据需要调整 userDto

          package com.zibo.common.util;
          import cn.hutool.jwt.JWT;
          import com.zibo.modules.user.dto.UserDto;
          import java.util.Date;
          import java.util.HashMap;
          import java.util.Map;
          import org.apache.commons.lang3.StringUtils;
          /**
           * JWT 工具类
           *
           * @author zibo
           * @date 2023/7/2 14:36
           * @slogan 慢慢学,不要停。
           */
          public class JWTUtility {
              /**
               * 密钥
               */
              private static final byte[] KEY = "zibo".getBytes();
              /**
               * 过期时间(秒):7 天
               */
              public static final long EXPIRE = 7 * 24 * 60 * 60;
              private JWTUtility() {
              }
              /**
               * 根据 userDto 生成 token
               *
               * @param dto    用户信息
               * @return token
               */
              public static String generateTokenForUser(UserDto dto) {
                  Map map = new HashMap<>();
                  map.put("id", dto.getId());
                  map.put("nickname", dto.getNickname());
                  return generateToken(map);
              }
              /**
               * 根据 map 生成 token 默认:HS265(HmacSHA256)算法
               *
               * @param map    携带数据
               * @return token
               */
              public static String generateToken(Map map) {
                  JWT jwt = JWT.create();
                  // 设置携带数据
                  map.forEach(jwt::setPayload);
                  // 设置密钥
                  jwt.setKey(KEY);
                  // 设置过期时间
                  jwt.setExpiresAt(new Date(System.currentTimeMillis() + EXPIRE * 1000));
                  return jwt.sign();
              }
              /**
               * token 校验
               * @param token token
               * @return 是否通过校验
               */
              public static boolean verify (String token) {
                  if (StringUtils.isBlank(token)) return false;
                  return JWT.of(token).setKey(KEY).verify();
              }
              /**
               * token 校验,并获取 userDto
               * @param token token
               * @return userDto
               */
              public static UserDto verifyAndGetUser(String token) {
                  if(!verify(token)) return null;
                  // 解析数据
                  JWT jwt = JWT.of(token);
                  Long id = Long.valueOf(jwt.getPayload("id").toString());
                  String nickname = jwt.getPayload("nickname").toString();
                  // 返回用户信息
                  return new UserDto(id, nickname);
              }
          }
          

          2、在拦截器中校验和解析 token

          package com.zibo.common.config;
          import com.zibo.common.enums.AppCode;
          import com.zibo.common.pojo.ApiException;
          import com.zibo.common.pojo.UserHolder;
          import com.zibo.common.util.JWTUtility;
          import com.zibo.modules.user.dto.UserDto;
          import javax.servlet.http.HttpServletRequest;
          import javax.servlet.http.HttpServletResponse;
          import lombok.extern.slf4j.Slf4j;
          import org.apache.commons.lang3.StringUtils;
          import org.springframework.stereotype.Component;
          import org.springframework.web.servlet.HandlerInterceptor;
          /**
           * 自定义拦截器
           * @author Administrator
           */
          @Component
          @Slf4j
          public class UserInterceptor implements HandlerInterceptor {
              /**
               * 进入controller方法之前执行。如果返回false,则不会执行 controller 的方法
               *
               * @param request 请求
               * @param response 响应
               * @param handler 处理器
               * @return 是否放行
               */
              @Override
              public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
                  // 获取 header 中的 Authorization 信息
                  String token = request.getHeader("token");
                  if (StringUtils.isNotBlank(token)) {
                      UserDto dto = JWTUtility.verifyAndGetUser(token);
                      if (dto != null) {
                          UserHolder.setUserInfo(dto);
                      } else {
                          log.error("token 验证失败!token is {}, uri is {}", token, request.getRequestURI());
                          throw new ApiException(AppCode.TOKEN_ERROR, "token 校验不通过!");
                      }
                  } else {
                      log.error("token 验证失败!token is {}, uri is {}", token, request.getRequestURI());
                      throw new ApiException(AppCode.TOKEN_ERROR, "token 为空!");
                  }
                  return true;
              }
              /**
               * 响应结束之前
               * @param request 请求
               * @param response 响应
               * @param handler 处理器
               */
              @Override
              public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
                  // 清理掉当前线程中的数据,防止内存泄漏
                  UserHolder.remove();
              }
          }
          

          3、在注册和登录时签发 token

          @PostMapping("/loginOrRegister")
          public UserDto loginOrRegister(@RequestBody @Validated UserDto dto) {
              // 通过手机号查询
              UserDto byPhone = service.findByPhone(dto.getPhone());
              // 如果操作标记为空,则报错
              if (ObjectUtils.isEmpty(dto.getOperation())) {
                  throw new ApiException("操作标记不能为空!");
              }
              // 如果是注册
              if (dto.getOperation() == 0) {
                  // 如果用户已经存在,则报错
                  if (ObjectUtils.isNotEmpty(byPhone)) {
                      throw new ApiException("注册失败!账号已存在!");
                  }
                  // 创建用户
                  Long save = service.save(dto);
                  // 返回用户信息
                  UserDto userDto = service.findById(save);
                  // 根据用户生成 token
                  String token = JWTUtility.generateTokenForUser(userDto);
                  // 保存到 redis
                  service.saveToken(userDto.getId(), token);
                  // 设置 token
                  userDto.setToken(token);
                  LogUtility.baseInfoWith("注册成功!" + userDto);
                  return userDto;
              }
              // 登录
              if (ObjectUtils.isEmpty(byPhone)) {
                  log.error("登录失败!账号不存在!" + dto);
                  // 账号不存在
                  throw new ApiException("登录失败!账号不存在!com/zibo/controller/user/UserController.java:62");
              } else {
                  // 比较密码是否一致
                  if (!byPhone.getPassword().equals(dto.getPassword())) {
                      throw new ApiException("登录失败!账号或密码错误!");
                  }
                  // 更新最后登录时间
                  byPhone.setLastLoginTime(LocalDateTime.now());
                  service.save(byPhone);
                  // 从 redis 获取 token
                  String token = service.getToken(byPhone.getId());
                  if (StringUtils.isBlank(token)) {
                      // 根据用户生成 token
                      token = JWTUtility.generateTokenForUser(byPhone);
                      log.info("用户登录,并生成token,id 为:{}, 昵称为:{},token 为:{}", byPhone.getId(), byPhone.getNickname(), token);
                      // 保存到 redis
                      service.saveToken(byPhone.getId(), token);
                  }
                  // 设置 token
                  byPhone.setToken(token);
                  LogUtility.baseInfoWith("登录成功!" + byPhone);
                  return byPhone;
              }
          }
          

          4、用户信息 UserHolder

          package com.zibo.common.pojo;
          import com.zibo.modules.user.dto.UserDto;
          /**
           * 存放用户信息的容器
           * @author Administrator
           */
          public class UserHolder {
              private static final ThreadLocal THREAD_LOCAL = new ThreadLocal<>();
              private UserHolder() {
              }
              /**
               * 获取线程中的用户
               * @return 用户信息
               */
              public static UserDto getUserInfo() {
                  return THREAD_LOCAL.get();
              }
              /**
               * 设置当前线程中的用户
               * @param info 用户信息
               */
              public static void setUserInfo(UserDto info) {
                  THREAD_LOCAL.set(info);
              }
              public static Long getUserId() {
                  UserDto dto = THREAD_LOCAL.get();
                  if (dto != null) {
                      return dto.getId();
                  } else {
                      // 注册或登录时没有,返回 0
                      return 0L;
                  }
              }
              public static void remove() {
                  THREAD_LOCAL.remove();
              }
          }