[SpringBoot] integration of SpringBoot + JWT+ Mybatis

Now we need to complete the following requirements:

After the user executes the login interface, a token needs to be generated and returned to the front end. Then, the front end carries the token in the request header to request other background interfaces.

Before completing this requirement, let's first understand what JWT is.

1. Understand JWT

Introduction to JWT 1.1

Introduction to JWT:

The full name of JWT is Jason web token. It is a form defined in RFC 7519 for securely transmitting information as a Json object. The information stored in JWT is "digitally signed", so it can be trusted and understood. JWT can be signed using HMAC algorithm or public / private key of RSA/ECDSA.

JWT function:

JWT has the following two functions:

  1. Authentication: once the user logs in, each subsequent request will contain JWT, allowing the user to access the routes, services and resources allowed by the token. Single sign on is a feature of JWT that is widely used today because of its low overhead.
  2. Information exchange: JWT is a way to safely transmit information. JWT is signed and authenticated by using public / private key. In addition, because the signature is calculated using head and payload, you can also verify whether the content has been tampered with.

1.2 structure of JWT

JWT is mainly composed of three parts, each of which is made of The parts are divided into:

  • Header header
  • Payload
  • Signature

Therefore, JWT is usually: XXX yyyy. zzzzz

For example:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

1.2.1 Header

Header is the header of JWT, including two parts of information:

  • Token type: JWT
  • Encryption algorithm: HMAC SHA256 or RSA

For example, the default header of JWT is:

{
    "alg": "HS256",  // algorithm
    "typ": "JWT"     // type
}

Then the Header is base64 encoded to form the first part of JWT:

eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9
Base64

In Java, Base64 coding can be implemented. As follows:

This is Base64 encoding and decoding of the default header of JWT:

public class Test {
    public static void main(String[] args) throws Exception{
        Base64.Encoder encoder = Base64.getEncoder();
        Base64.Decoder decoder = Base64.getDecoder();

        String header = "{\"alg\": \"HS256\", \"typ\": \"JWT\"}";
        byte[] headerBytes = header.getBytes();
        // code
        String encodeHeader = encoder.encodeToString(headerBytes);
        System.out.println(encodeHeader);
        // decode
        byte[] decode = decoder.decode(encodeHeader);
        System.out.println(new String(decode, "UTF-8"));
    }
}

1.2.2 Payload

Payload contains a declaration. Declarations are declarations about entities (usually users) and other data. Similarly, it uses base64 encoding to form the second part of JWT. There are three types of declarations: registered, public, and private

1. registered declaration

registered declaration: it contains a set of predefined declarations recommended to use, mainly including:

  • iss: jwt issuer
  • Sub: the user JWT is targeting
  • aud: the party receiving jwt
  • Exp: the expiration time of JWT, which must be greater than the issuing time
  • nbf: define the time before which this jwt is unavailable
  • IAT: issuing time of JWT
  • JTI: the unique identity of JWT, which is mainly used as a one-time token to avoid replay attacks.

2. public declaration

Public declaration: any information can be added to the public declaration. Generally, the user's relevant information or other necessary information for business needs can be added, but it is not recommended to add sensitive information, because this part can be decrypted at the client.

3. private statement

private declaration: a custom declaration designed to share information between parties who agree to use them. It is neither a registration declaration nor a public declaration.

For example:

{
    "sub": "1234567890",
    "name": "John Doe",
    "iat": 1516239022
}
  • name: custom field
  • sub/iat: standard statement

Then the Payload is base64 encoded to form the second part:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

1.2.3 Signature

Signature refers to visa information, which consists of three parts:

  • Header (after Base64)
  • Payload (after Base64)
  • secret

For example, we need HMAC SHA256 algorithm for signature:

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

Signature is used to verify that the message has not changed during this process, and for tokens signed with the private key, it can also verify the true identity of the sender of the JWT

2. Use JWT

Import dependency:

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>

Here is the test class of SpringBoot. It can be different from me here. Just change these two methods to main.

public class AppTest {

    // Key (complex)
    private static final String SECRET = "1qazXSW2";

    @Test
    public void createToken() {
        Map<String, Object> map = new HashMap<>();
        Calendar expireTime = Calendar.getInstance();
        expireTime.add(Calendar.SECOND, 2000);

        String token = JWT.create()
                // Header (the default data is used, so the map has no value. This line of code can also be omitted)
                .withHeader(map)
                // Payload
                .withClaim("userId", 666)
                .withClaim("username", "zzc")
                // Expiration time
                .withExpiresAt(expireTime.getTime())
                // Signature
                .sign(Algorithm.HMAC256(SECRET));
        System.out.println(token);
    }

    @Test
    public void verifyToken() {
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
        String token = "";
        DecodedJWT decodedJWT = jwtVerifier.verify(token);
		// Get the information in the token
        System.out.println(decodedJWT.getClaim("userId").asInt());
        System.out.println(decodedJWT.getClaim("username").asString());
    }
}

explain:

  1. createToken(): method of generating token. As the second part of token, Payload can store information. So here, I put userId and username into the token. Later, you can obtain the corresponding value according to the token.
  2. verifyToken(): the method to verify the token. DecodedJWT decodedJWT = jwtVerifier.verify(token); If the token verification fails, this line of code will throw an exception; Otherwise, the execution will be successful and continue. Then, you can get the information decodedjwt in the token getClaim("username"). asString().

3. Spring boot integrates JWT

[development environment]

  • IDEA-2020.2
  • SpringBoot-2.5.5
  • MAVEN-3.5.3
  • Mybatis
  • Mysql

[project structure chart]

1. Introduce dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--jwt-->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>
<!--mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.0.0</version>
</dependency>
<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.38</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

2. Add application YML configuration

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/zzc?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    password: root
    username: root
    driver-class-name: com.mysql.jdbc.Driver

mybatis:
  type-aliases-package: com.zzc.entity # Entity class alias
  mapper-locations: classpath:mapper/*.xml # mapper configuration file (required)
  configuration:
    map-underscore-to-camel-case: true # Hump naming

3. Add JWT tool class

Abstract the token generation method and token verification method into a tool class:

public class JwtUtil {

    private static final String SECRET = "1qazXSW2";

    // Generate Token
    public static String createToken(Map<String, String> paramMap) {
        Map<String, Object> headMap = new HashMap<>();
        Calendar expireTime = Calendar.getInstance();
        // The default expiration time is 7 days
        expireTime.add(Calendar.DATE, 7);

        JWTCreator.Builder builder = JWT.create();
        // Header (the default data is used, so the map has no value and can be omitted)
        builder.withHeader(headMap);
        // Payload
        paramMap.forEach((key, value) -> {
            builder.withClaim(key, value);
        });
        // Expiration time
        String token = builder.withExpiresAt(expireTime.getTime())
                // Signature
                .sign(Algorithm.HMAC256(SECRET));
        return token;
    }

    // Verify the legitimacy of token
    public static void verify(String token) {
        JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
    }

    // Get token information
    public static DecodedJWT getTokenInfo(String token) {
        return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
    }

}

4. Add Controller class

There are two interfaces in this Controller class:

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/login")
    public Map<String, Object> login(User user) {
        log.info("User name:[{}]", user.getName());
        log.info("Password:[{}]", user.getPwd());
        Map<String, Object> map = new HashMap<>();
        try {
            User u = userService.login(user);

            // Generate token
            Map<String, String> payload = new HashMap<>();
            payload.put("id", u.getId());
            payload.put("name", u.getName());
            String token = JwtUtil.createToken(payload);

            map.put("status", true);
            map.put("msg", "Authentication successful");
            map.put("token", token);
        } catch (Exception e) {
            map.put("status", false);
            map.put("msg", e.getMessage());
        }
        return map;
    }

    @PostMapping("/testToken")
    public Map<String, Object> testToken(String token) {
        log.info("Current login token: [{}]", token);
        Map<String, Object> map = new HashMap<>();
        try {
            JwtUtil.verify(token);
            map.put("status", true);
            map.put("msg", "Request succeeded");
            return map;
        } catch (SignatureVerificationException e) {
            log.error("[Request failed]:{}", e.getMessage());
            map.put("msg", "Invalid signature");
        } catch (TokenExpiredException e) {
            log.error("[Request failed]:{}", e.getMessage());
            map.put("msg", "token be overdue");
        } catch (AlgorithmMismatchException e) {
            log.error("[Request failed]:{}", e.getMessage());
            map.put("msg", "token Inconsistent algorithm");
        } catch (Exception e) {
            log.error("[Request failed]:{}", e.getMessage());
            map.put("msg", "token invalid");
        }
        map.put("status", false);
        return map;
    }
}

explain:

  1. login(): after the user logs in successfully, a token is generated through JWT. Then, the id and name of the user are stored in the token, and then the token is directly returned to the front end
  2. testToken(): when requesting this interface, the request header needs to carry the token jwtutil verify(token);, Otherwise, an error will be reported.

5. Add UserServiceImpl class

UserService interface omitted

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public User login(User user) {
        User u = userMapper.login(user);
        if (null != u) {
            return u;
        }
        throw new RuntimeException("Authentication failed");
    }
}

6. Add UserMapper interface

public interface UserMapper {
    User login(User user);
}

The corresponding usermapper XML file:

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzc.mapper.UserMapper">

    <select id="login" parameterType="User" resultType="User">
        SELECT
            id,name,pwd
        FROM TAB_USER
        WHERE 1=1
        AND name = #{name}
        AND pwd = #{pwd}
    </select>

</mapper>

Query user information through user name and password.

7. Run code

7-1. Call login interface


After successful authentication, a token is returned. The front end can save the token and carry it in the request header.

7-2. Call other interfaces


If the request does not carry a token, the request will fail "token is invalid".


If a token is carried in the request, the request will succeed.

8. Optimize code

Do you usually need to write one or more interfaces in the background to verify the redundancy of each project?

Obviously, not so!

At this time, we thought of the Interceptor. As follows:

8-1. Add an interceptor

@Slf4j
public class JwtInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Map<String, Object> map = new HashMap<>();
        String token = request.getHeader("token");
        try {
            JwtUtil.verify(token);
            // Release
            return true;
        } catch (SignatureVerificationException e) {
            log.error("[Request failed]:{}", e.getMessage());
            map.put("msg", "Invalid signature");
        } catch (TokenExpiredException e) {
            log.error("[Request failed]:{}", e.getMessage());
            map.put("msg", "token be overdue");
        } catch (AlgorithmMismatchException e) {
            log.error("[Request failed]:{}", e.getMessage());
            map.put("msg", "token Inconsistent algorithm");
        } catch (Exception e) {
            log.error("[Request failed]:{}", e.getMessage());
            map.put("msg", "token invalid");
        }
        // Return the error message to the foreground
        map.put("status", false);
        // Convert Map to json string
        String errorResult = new ObjectMapper().writeValueAsString(map);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(errorResult);
        return false;
    }

}

If the token verification fails, the failure information will be returned to the foreground.

8-2. Add an interceptor configuration class

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JwtInterceptor())
                .addPathPatterns("/user/testToken2")
                .excludePathPatterns("/user/login");
    }
}

explain:

  • The above configuration is used to intercept the url of: / user/testToken2/ user/login does not intercept.

Well, SpringBoot + JWT is here.

Source address

Keywords: Java Spring Spring Boot

Added by Paulkirkewalker on Sun, 06 Feb 2022 23:47:01 +0200