你还不会在SpringBoot中使用jwt实现token身份认证吗?

2023年 9月 2日 67.4k 0

  • jwt (JSON Web Tokens)

jwt结构

  • 用(.)分割的三个部位组成
    • 标头
    • 有效载荷
    • 签名

xxx.xxx.xxx

标头

  • 标头通常由两部分组成:令牌的类型(JWT)和所使用的签名算法(例如 HMAC SHA256 或 RSA
{ "alg": "HS256", "typ": "JWT" }
  • 然后,对该 JSON 进行Base64Url编码以形成 JWT 的第一部分。

有效载荷

  • 令牌的第二部分是有效负载,其中包含声明。声明是关于实体(通常是用户)和附加数据的声明。索赔分为三种类型:注册索赔、公开索赔和私人索赔。
  • 注册声明
    • 这些是一组预定义的声明,不是强制性的,而是推荐的,以提供一组有用的、可互操作的声明。其中一些是: iss(发行者)、 exp(到期时间)、 sub(主题)、 aud(受众)等。
  • 公共声明
    • 这些可以由使用 JWT 的人随意定义。但为了避免冲突,它们应该在[IANA JSON Web Token Registry]中定义,或者定义为包含防冲突命名空间的 URI
  • 私人声明
    • 这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,既不是注册声明也不是公开声明。
  • 有效负载示例

    { "sub": "1234567890", "name": "John Doe", "admin": true }
    
    • 然后对有效负载进行Base64Url编码以形成 JSON Web 令牌的第二部分。

    请注意,对于签名令牌,此信息虽然受到防止篡改的保护,但任何人都可以读取。除非加密,否则请勿将秘密信息放入 JWT 的有效负载或标头元素中。

    签名

    • 要创建签名部分,您必须获取编码的标头、编码的有效负载、秘密、标头中指定的算法,然后对其进行签名。

    使用HMAC SHA256算法,则将通过以下方式创建签名

    HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
    

    签名用于验证消息在传输过程中没有发生更改,并且在使用私钥签名的令牌的情况下,它还可以验证 JWT 的发送者是否是其所说的人。

    将所有内容放在一起

    • 输出是三个由点分隔的 Base64-URL 字符串,可以在 HTML 和 HTTP 环境中轻松传递,同时与基于 XML 的标准(例如 SAML)相比更加紧凑。
    • 下面显示了一个 JWT,它具有先前的标头和有效负载编码,并且使用密钥进行签名。

    image.png

    • 官网翻译的,先了解一点前置知识,开整!

    使用

    依赖

    • pom.xml文件咱们先加入jwt的依赖

    image.png

    JwtTokenUtils工具类

    public class JwtTokenUtils {
      // token私钥
      private static final String TOKEN_SECRET = "jwtSECRET";
      // token过期时长30分钟
      private static final long EXPIRE_TIME = 1800L; // 单位为秒
      /**
       * 生成用户token,设置token超时时间
       *
       * @return 生成的token
       */
       public static String createToken(LoginVo loginVo) {
        Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME * 1000);
        // 创建 Token 的 payload
        Map map = new HashMap();
        map.put("alg", "HS256");
        map.put("typ", "JWT");
        // 生成 JWT Token
        String token = JWT.create()
          .withHeader(map)// 添加头部
          .withClaim("account", loginVo.getAccount())
          .withExpiresAt(expireDate) //超时设置,设置过期的日期
          .withIssuedAt(new Date()) //签发时间
          .sign(Algorithm.HMAC256(TOKEN_SECRET)); //SECRET加密
        return token;
      }
      /**
       * 检验token是否正确
       * @param token 需要校验的token
       * @return 校验是否成功
       */
      public static Map verifyToken(String token) {
        DecodedJWT jwt = null;
        token = token.replace("Bearer ", ""); // 这里要去掉Bearer ,postman默认会加这个,导致匹配不上
        try {
          //设置签名的加密算法:HMAC256
          Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); // 私钥需要一致
          JWTVerifier verifier = JWT.require(algorithm).build();
          jwt = verifier.verify(token);
        } catch (Exception e) {
    //      logger.error(e.getMessage()); // 还没安装log4j等日志依赖,先注释
    //      logger.error("token解码异常");
          //解码异常则抛出异常
          return null;
        }
        return jwt.getClaims();
      }
    }
    
  • createToken方法主要就是创建token,并把账号密码作为自定义声明添加到 JWT 的 payload 中,最终返回
  • verifyToken方法其实就是解码 token 并将其转换成 DecodedJWT 对象,通过 getClaims() 方法获取 payload 中的所有声明信息,并以 Map 的形式返回,如果 token 不合法或者解码过程中出现异常,则返回 null,校验的效果。
    • 注意Bearer 这个玩意

    JwtFilter.java

    @Component
    @WebFilter(filterName = "JwtFilter", urlPatterns = "/*")
    public class JwtFilter implements Filter {
      @Override
      public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("jwt初始化方法");
      }
    
      @Override
      public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) req;
        final HttpServletResponse response = (HttpServletResponse) res;
    
        response.setCharacterEncoding("UTF-8");
        final String token = request.getHeader("Authorization");
    
        String requestPath = request.getRequestURI();
        // 登录接口,直接放行
        if (requestPath.contains("/user/login")) {
          chain.doFilter(request, response);
          return;
        }
         if ("OPTIONS".equals(request.getMethod())) { // 是否是 OPTIONS 请求
          response.setStatus(HttpServletResponse.SC_OK);
          chain.doFilter(request, response);
        } else {
          if (token == null) {
            response.getWriter().write("没有令牌");
            return;
          }
          Map userData = JwtTokenUtils.verifyToken(token); // 检验 token
          if (userData == null) {
            response.getWriter().write("令牌非法");
            return;
          }
          String account = userData.get("account").asString(); // 获取用户名
          request.setAttribute("account", account); // 设置用户名
          chain.doFilter(req, res); // 过滤成功
        }
      }
      @Override
      public void destroy() {
        System.out.println("jwt销毁方法执行的逻辑");
      }
    }
    
    • 这其实就是一个过滤类,你请求接口前就会先到这里,执行相对应的逻辑。
  • init: 初始化方法
  • doFilter:过滤拦截方法(主要逻辑就写在这)
  • destroy:销毁方法
  • 注意

  • urlPatterns = "/*" : 这个代表拦截所有的请求,我在doFilter里面判断了包含/login直接放行。意思就是登录接口直接放行,返回token给前端,如果是别的请求则走对应逻辑。
  • @Component:这个需要加上,这样才能扫描到你这个类不然不生效的。或者你在启动类加上@ServletComponentScan(basePackages = "com.yjx.boot.config"),我的过滤器是写在config包下的。
  • LoginController

    @Controller
    @RestController  // 可处理 HTTP 请求的控制器,并且能够返回数据给客户端
    @RequestMapping("/user")
    @RequiredArgsConstructor
    public class LoginController {
      private final AdminService adminService;
      // 用户登陆
      @GetMapping("/login")
      ResultVO login(LoginVo loginVo) {
        Admin admin = new Admin();
        admin.setAccount(loginVo.getAccount());
        admin.setAdminPassword(loginVo.getAdminPassword());
        // 查找是否有该管理员
        Admin an = adminService.queryByAdmin(admin);
        if (an == null) {
          return ResultResponse.failure(ResultCodeEnum.INTERNAL_NOT_FOUND);
        } else {
          String token = JwtTokenUtils.createToken(loginVo);
          loginVo.setToken(token);
          an.setToken(token);
          return ResultResponse.success(an);
        }
      }
      @GetMapping("/secure/current_registrant")
      public String currentRegistrant(HttpServletRequest request) {
        System.out.println("request" + request);
        String id = request.getParameter("id");
        String account = request.getParameter("account");
        String user_password = request.getParameter("adminPassword");
        return "当前用户信息:   ,account=" + account + " ,user_password=" + user_password;
      }
    }
    
    • 代码部分就完事了,测试一下。

    测试

    • 我们请求下/secure/current_registrant接口,此时是没有token的。
      image.png

    正常,输出了我们想要的:没有令牌

    • 我们先去请求/login,看看token以及用户信息会不会如我们所想 返回给我们呢?

    image.png

    很好,也拿到了我们想要的,token也返回了,我们复制一下这个token,我们再次请求/secure/current_registrant,并把token放入请求头,试试能不能成功

    image.png

    • 成功

    我们来试试把token写错一个字母会输出什么。

    image.png

    我们在请求头不传入token呢

    image.png

    完事!测试成功!写的并没有很好 甚至可能有些地方都写错了,慢慢完善吧

    • 最后说一下项目中大致流程

    后端登录接口一般是不做拦截的,因为需要通过登录接口获取到token,除登录以外其他的接口都做了拦截,请求都需要携带token请求,如果没有请求头没有携带对应token,则会返回没有令牌信息,也不会返回数据,请求头携带了token并且token正确,才会返回对应信息。

  • 后端编写登录接口
  • 前端登录输入信息,发起请求携带账号密码信息给后端
  • 后端收到前端的账号密码,去数据库校验用户账号密码是否正确匹配,匹配上了则为成功,并且返回对应用户信息以及token
  • 前端请求成功收到token,把token存储起来,放在请求头,请求其他接口就可以成功获取到对应数据。
  • 2023-09-02

    问题一:过滤器直接返回中文乱码

    image.png

    • 前端请求在浏览器查看乱码
      image.png

    解决

    response.setContentType("text/plain;"); // 解决中文乱码
    

    image.png

    • 在测试一下

    image.png

    优化

    • 可以看到问题一的提示也不太友好,我们稍作改动

    image.png

    • 这样是不是好很多了呢?贴一下相关代码
      JwtFilter.java

    image.png

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论