• 【Springboot】Vue3-Springboot引入JWT实现登录校验以及常见的错误解决方案


    前言

    项目版本:
    后端: Springboot 2.7、 Mybatis-plus、Maven 3.8.1
    数据库:MySQL 8.0
    前端:Vue3、Axois 1.6.0 、Vite 4.5.0、Element-Plus、Router-v4

    一、JWT简单介绍

    JWT 全称 JSON Web Token,是一种基于 JSON 的数据对象,通过技术手段将数据对象签名为一个可以被验证和信任的令牌(Token)在客户端和服务端之间进行安全的传输。

    二、token校验设计思路

    1. 首先,用户从登录请求发往后端后,后端生成token,并将token返回给前端。
    2. 前端拿到后端生成的token后,保存在localStorage中,在token时效内,用户拿着这个token访问系统所有的功能。
    3. 一旦token失效,系统将会强制用户退出系统,直到重新登录才能获取新的token.如此循环。

    三、使用步骤

    Springboot部署JWT

    在整个JWT token的周期中,只需要在用户登录的时候生成token,其余访问页面均用vue3的路由守卫拦截,用户向后端发起的请求中都会携带token,后端只有token校验合法后才会执行具体的业务。

    引入依赖:

        <dependency>
                <groupId>io.jsonwebtokengroupId>
                <artifactId>jjwtartifactId>
                <version>0.9.1version>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    创建登录实体类

    package com.fy36.hotelmanage.entity;
    
    import com.baomidou.mybatisplus.annotation.IdType;
    import com.baomidou.mybatisplus.annotation.TableField;
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    import lombok.Data;
    import lombok.ToString;
    
    @Data
    @ToString
    @TableName(value = "tbadmin")
    public class Admin {
        private String username;
        private String password;
        @TableId(type = IdType.AUTO)
        private Long Id;
        @TableField(exist = false) //token字段不映射到数据库,只是用来携带。
        private String token;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    上面的@TableFiled exist=false,表示不对照对应的sql字段,因为token校验并不存到数据库中,只是用来存储到用户实体中,发往前端校验。如果不添加该字段,将会报错"Unkown column if filed list in ‘tbadmin.token’.如下是admin表中的字段设计。

    在这里插入图片描述

    1. 创建JwtUtils.java
    public class JWTUtils {
        private static long TIME = 1000 * 5; //token有效期,以毫秒为单位,所以这里token有效期为5s.
        private static String SIGNATURE = "2786"; //私钥,签名
        public static String createToken(Admin admin) {
            JwtBuilder jwtBuilder = Jwts.builder(); //构建jwt对象
            //配置header
            String jwtToken = jwtBuilder
                    //配置hader
                    .setHeaderParam("alg", "HS256") //签名算法
                    .setHeaderParam("typ", "JWT")   //TYPE 为JWT
                    //payload,载荷,不要加入隐私信息
                    .claim("username", admin.getUsername()).setExpiration(new Date(System.currentTimeMillis() + TIME)) //假定token有效时间为24x小时
                    //signature
                    .signWith(SignatureAlgorithm.HS256, SIGNATURE)
                    //拼接该三部分,构成一个完整的token
                    .compact();
            return jwtToken;
        }
    
        public static boolean checkToken(String token) {
            if (token == null) {
                return false;
            }
            try {
                Jws<Claims> claimsJws = Jwts.parser().setSigningKey(SIGNATURE).parseClaimsJws(token);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
            return true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    如下是前端点击登录按钮后,触发的登录方法:

    //测试请求方法
    const login = function () {
      //测试样例2
      api
        .post("/login", {
          ...formLabelAlign,
        })
        .then(function (res) {
          if (res.data.code == 200) {
            ElMessage.success("登录成功!");
            //用户登录成功后,将后端生成的token,放到localStorage中。
            //如果收到了后端发送过来token,那么存储token,并跳转到系统界面。
            if (res.data.data.token) {
              console.log("输出res");
              console.log(res.data);
              localStorage.setItem(
                "token_access",
                JSON.parse(JSON.stringify(res.data.data.token))
              );
            }
            //存储好token后,进入系统。
            router.push("/home");
          } else {
            ElMessage.error("用户名或密码错误,请重新输入");
          }
        });
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    用户点击后,向后端发送请求,对应/login接口

    后端:LoginController.java

    当用户名和密码都正确后,使用JwtUtils.class 生成token,并将它存放到Admin实体类中的token 字段中,发往前端。

        @PostMapping("/login")
        public ApiResult login(@RequestBody Admin admin) {
    
            Admin adminRes = loginService.adminLogin(admin);
            if (adminRes != null) {
                //设置token,发往前端口
                adminRes.setToken(JWTUtils.createToken(adminRes));
                System.out.println("后端生成的token为:\n" + adminRes.getToken());
                return ApiResultHandler.buildApiResult(200, "请求成功", adminRes);
            } else return ApiResultHandler.buildApiResult(400, "请求失败", "用户名账号或密码错误");
    
    /**
    校验token
    **/
        @GetMapping("/checkToken")
        public boolean checkToken(HttpServletRequest request) {
            System.out.println("reqeust:---------");
            System.out.println(request.toString());
            String token = request.getHeader("token");
            System.out.println("本地拿到的前端token为:");
            System.out.println(token);
            token = token.replaceAll("\"", "");
            System.out.println("处理后的token为:");
            System.out.println(token);
            boolean res = JWTUtils.checkToken(token);
            System.out.println("校验结果" + res);
            return res;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    OK,此时前端收到了token,会将它存放在localStorage中。
    在这里插入图片描述

    对应前端的login片段为:

      localStorage.setItem(
                "token_access",
                JSON.parse(JSON.stringify(res.data.data.token))
              );
    
    • 1
    • 2
    • 3
    • 4

    JSON.stringify()方法可以用于将JavaScript对象转换为字符串以便在网络上进行传输或存储。它还可以用于将JavaScript对象转换为字符串以便进行数据的序列化和持久化存储。

    如图,在谷歌浏览器,F12打开控制台–Application中,可以查看存放的token.
    (token不带引号)

    在这里插入图片描述
    此时用户成功进入系统,可以携带有效时期的token进行访问系统功能,但是用户每次点击系统其他功能时,将会校验token是否合法,主要检测的是token时效,如果超过这个时效,将会强制退出。那么,怎么让系统在每次用户请求时,都能自动发送给后端检验token呢?

    这里用到的是router路由守卫函数router.beforeEach((to, from, next)

    路由守卫函数,写在了main.js中.当用户每次调用后端服务时,都会携带已保存的token,发往后端,这里将token,放入了请求头中,后端使用HttpServletRequest 来获取请求头.(代码看上面的LoginController.class)

    路由守卫函数

    路由守卫函数:

    //进行任何跳转前,都需要进行该方法的调用。
    ... 
    const router = createRouter({
      history: createWebHistory(),
      routes,
    });
    
    router.beforeEach((to, from, next) => {
      if (to.path == "/login") {
        // 登录或者注册才可以往下进行
        window.localStorage.removeItem("token_access");//移除token
        next();
      } else {
        // 获取 token
        let admin_token = JSON.stringify(
          window.localStorage.getItem("token_access")
        );
        // token 不存在
        if (admin_token === null || admin_token === "") {
          ElMessage.error("您还没有登录,请先登录");
          next("/login");
        } else {
          //校验token合法性
          api.get("/checkToken", {
              headers: {
                token: admin_token,
              },
            })
            .then(function (res) {
              if (res.data) {
                //token校验发现合法
                console.log("token合法");
                // router.push("/home");
              } else {
                ElMessage.error("token校验不合法,请重新登录");
                localStorage.removeItem("token_access");
                router.push("/login");
              }
            })
            .catch(function (error) {
              ElMessage.error("token已失效,重新登陆!");
              console.log(error);
            });
          next();
        }
      }
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    有一点需要注意的是,token在前后端交互的过程中,格式的变化,如下图是控制台输出的token交互中的变化。token放到header的方法博客

    本地拿到的前端token为:
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjExMSIsImV4cCI6MTY5OTQxOTAxMn0.Y7mbTlsp5dJ1-hKE8RCtviZwFIC3E_CdjhFPDsMT5Ws"
    
    处理后的token为:
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjExMSIsImV4cCI6MTY5OTQxOTAxMn0.Y7mbTlsp5dJ1-hKE8RCtviZwFIC3E_CdjhFPDsMT5Ws
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在token校验中,往往不是token时效导致校验失败,而因为前后端交互的token的格式不一致导致校验失败。前端传入的token需要在后端去除两边的双引号。

    StackOverflow的解决方案: Storing my API token in local storage is wrapping the token in double quotes

    四、问题

    问题1:io.jsonwebtoken.UnsupportedJwtException: Signed Claims JWSs are not supported
    问题就是:不支持已签名的声明JWS。
    如果使用 Jwts.builder() 创建token,在解析时,就需要使用 parseClaimsJws(token) 而不是 parseClaimsJwt(token) .

    问题2:JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
    中文意思是:JWT签名与本地计算的签名不匹配。无法断言JWT有效性,不应信任JWT有效性
    出现这种异常的情况正如上面所说,token是一串字符串且不带双引号,后端需要进行 token = token.replaceAll("\"", "");处理。

    问题3:Cannot access ‘res’ before initialization
    请根据提示,查看这个result变量,是否在 代码下文 中是否重新进行了let res 重新定义之类的操作。

    问题4:使用localStorage.getItem方法获取token时,发现token的形式外围包围了一层引号

    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
    
    • 1

    这是因为,没有使用JSON.parse,将该字符串转化为javascript对象。

    改写为:window.localStorage.getItem(JSON.stringify(token));
    
    • 1

    问题5:JWT expired at 2023-11-07T15:42:27Z. Current time: 2023-11-07T15:42:27Z, a difference of 105 milliseconds. Allowed clock skew: 0 milliseconds.
    这说明token 已经过期了,具体是在JWTUtils.java中的:

     String jwtToken = jwtBuilder
                    //配置hader
                    .setHeaderParam("alg", "HS256") //加密算法
                    .setHeaderParam("typ", "JWT")   //TYPE 为JWT
                    //payload,载荷,不要加入隐私信息
                    .claim("username", admin.getUsername())
                    .setExpiration(new Date(System.currentTimeMillis() + TIME)) //假定token有效时间为24x小时
                    //signature
                    .signWith(SignatureAlgorithm.HS256, SIGNATURE)
                    //拼接该三部分,构成一个完整的token
                    .compact();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    中的setExpiration ,这里new Date是毫秒级别的当前时间,该语句含义就是,在当前时间之后的TIME毫秒,是有效的。之前这里改了之后还是token过期,尝试先把TIME改的更大,然后重新运行一下Springboot。另外,还有一个token时间不生效的原因是,当前已经登录的用户token时效已经设定,此时需要执行· localStorage.removeItem("token_access") 来删除掉原有的token,重新登陆一次,新token就会根据当前时间来修改。

  • 相关阅读:
    微服务SpringBoot 整合Redis 实现点赞、点赞排行榜
    c#使用log4net的3种调用方法
    【Javascript】在对象的方法里访问自己的属性
    用Python写一个自动下载视频、弹幕、评论的软件(2022最新)
    5G小数据传输增强技术
    Educational Codeforces Round 133 (Rated for Div. 2)(CD题解)
    Ansys Speos | 手把手教你画光导
    五分钟搭建博客系统 OK?
    西门子200程序案例集
    JavaScript高级,ES6 笔记 第三天
  • 原文地址:https://blog.csdn.net/SKMIT/article/details/134286486