[Springboot] Vue3-Springboot introduces JWT to implement login verification and common error solutions

Preface

Project version:
Backend: Springboot 2.7, Mybatis-plus, Maven 3.8.1
Database: MySQL 8.0
Front-end: Vue3, Axois 1.6.0, Vite 4.5.0, Element-Plus, Router-v4

1. Brief introduction to JWT

JWT, the full name of JSON Web Token, is a JSON-based data object that uses technical means to sign the data object into a token that can be verified and trusted for secure transmission between the client and the server.

2. Token verification design ideas

1. First, after the user sends a login request to the backend, the backend generates a token and returns the token to the frontend.
2. After the front-end gets the token generated by the back-end, it saves it in localStorage. Within the token validity period, the user can use this token to access all functions of the system.
3. Once the token expires, the system will force the user to log out of the system until they log in again to obtain a new token. This cycle continues.

3. Usage steps

Springboot deploy JWT

In the entire JWT token cycle, the token only needs to be generated when the user logs in. All other visited pages are intercepted by vue3's routing guard. The user's request to the backend will carry the token, and the backend will only verify that the token is legal. Will perform specific tasks.

Introduce dependencies:

    <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

Create login entity class

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;

}

The above @TableFiled exist=false means that the corresponding sql field is not compared, because the token verification is not stored in the database, but is only used to store it in the user entity and send it to the front-end for verification. If this field is not added, the error "Unkown column if filed list in 'tbadmin.token'" will be reported. The following is the field design in the admin table.

Insert image description here

  1. Create 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;
    }
}

The following is the login method triggered after the front-end clicks the login button:

//测试请求方法
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("用户名或密码错误,请重新输入");
      }
    });
};

After the user clicks, a request is sent to the backend, corresponding to/logininterface

Backend: LoginController.java

When the username and password are correct, use JwtUtils.class to generate a token, store it in the token field in the Admin entity class, and send it to the front end.

    @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;
    }

OK, now the front end receives the token and stores it in localStorage.
Insert image description here

The login fragment corresponding to the front end is:

  localStorage.setItem(
            "token_access",
            JSON.parse(JSON.stringify(res.data.data.token))
          );

The JSON.stringify() method can be used to convert JavaScript objects into strings for transmission or storage over the network. It can also be used to convert JavaScript objects into strings for data serialization and persistent storage.

As shown in the picture, in Google Chrome, F12 opens the console-Application, and you can view the stored token.
(token without quotation marks)

Insert image description here
At this time, the user successfully enters the system and can bring a valid token to access system functions. However, every time the user clicks on other functions of the system, it will be verified whether the token is legal. The main test is the token validity. If this validity period is exceeded, it will Force quit. So, how can the system automatically send the backend verification token every time a user requests it?

The router routing guard function is used hererouter.beforeEach((to, from, next):

The route guard function is written in main.js. Every time the user calls the backend service, the saved token will be sent to the backend. Here, the token is put into the request header. The backend uses HttpServletRequest to obtain the request header. (See LoginController.class above for the code)

route guard function

Route guard function:

//进行任何跳转前,都需要进行该方法的调用。
... 
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();
    }
  }
});

One thing to note is that the token format changes during the interaction between the front and back ends. The following figure shows the changes in the token interaction output by the console. How to put token in header blog

本地拿到的前端token为:
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjExMSIsImV4cCI6MTY5OTQxOTAxMn0.Y7mbTlsp5dJ1-hKE8RCtviZwFIC3E_CdjhFPDsMT5Ws"

处理后的token为:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjExMSIsImV4cCI6MTY5OTQxOTAxMn0.Y7mbTlsp5dJ1-hKE8RCtviZwFIC3E_CdjhFPDsMT5Ws

In token verification, it is often not the token expiration that causes verification failure, but the inconsistent format of the tokens used for front-end and back-end interaction. The token passed in from the front end needs to have the double quotes on both sides removed at the back end.

StackOverflow's solution: Storing my API token in local storage is wrapping the token in double quotes

4. Questions

Problem 1: io.jsonwebtoken.UnsupportedJwtException: Signed Claims JWSs are not supported
The problem is: Signed Claims JWS are not supported.
If you use Jwts.builder() to create a token, you need to use parseClaimsJws(token) instead of parseClaimsJwt(token) when parsing .

Question 2: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
Chinese meaning: JWT signature does not match locally computed signature . The validity of JWT cannot be asserted and the validity of JWT should not be trusted
When this exception occurs, as mentioned above, the token is a string without double quotes, and the backend needs to be processed a> token = token.replaceAll("\"", ""); processing.

Question 3: Cannot access 'res' before initialization
Please follow the prompts to check whether the result variable is in the code below Whether operations such as redefining have been performed again in . let res

Question 4: When using the localStorage.getItem method to obtain the token, it is found that the token form is surrounded by a layer of quotation marks.

"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"

This is because JSON.parse is not used to convert the string into a javascript object.

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

Question 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.
This means that the token has expired, specifically in 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();

setExpiration in , where new Date is the current time in milliseconds. The meaning of this statement is that TIME milliseconds after the current time are valid. After changing this before, the token still expires. Try changing the TIME to a larger value, and then re-run Springboot. In addition, another reason why the token time does not take effect is that the token time limit of the currently logged in user has been set. At this time, you need to execute · localStorage.removeItem("token_access") to delete the original token and log in again. The new token will be modified based on the current time.

Guess you like

Origin blog.csdn.net/SKMIT/article/details/134286486