This article is participating in “Java Theme Month – Java Development in Action”, see the activity link for details

preface

For JWT (JSON Web Token), you can test your understanding of JWT by asking yourself the following questions:

1. What is the principle of JWT function implementation?

2. What are the advantages and disadvantages of using JWT?

3. If JWT is used in the project, can the necessity of each part of it be recognized?

If you’re not clear, you can look down with questions.

Finally, a JWT instance will be done in conjunction with Spring Boot

1. JWT principle exploration

What is JWT? The official definition is

JSON Web token (JWT) is an open standard ‎ [‎ (RFC 7519 (https://tools.ietf.org/html/rfc7519) ‎ ‎]), it defines a compact and self-contained way, Use to securely transfer information between parties as JSON objects.Copy the code

Remove all the fancy descriptions and you have a standard for secure communication between clients and servers.

1.1 Structure and composition of JWT

The JWT consists of three parts connected by a period (.), header.payload-signature. All three parts are base64 encoded to ensure secure transport in urls.

The first part of header is composed of two parts of information, that is, declare the type JWT and declare the encryption algorithm (for example, SHA256), so the header is the following JSON before the base64URL encoding

{
    'typ':'jwt',
    'alg':'SHA256'
}
Copy the code

After base64URL encoding:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Copy the code

The second part of the payload is used to store information, such as the user ID and the outdated time exp. Therefore, the payload is the following JSON before the base64URL encoding

{
	'id':'10',
	'exp':'2301597433'}Copy the code

After base64URL encoding:

ewoJJ2lkJzonMTAnLAoJJ2V4cCc6JzIzMDE1OTc0MzMnCn0=
Copy the code

It is composed of the following elements: 1. The header and the payload are encoded in the base64URL. 2. Add a signature string that only the server knows. 3. Use the SHA256 signature algorithm in the header to encrypt data. It can be viewed as the following formula:

signature = SHA256(base64encode(header) + '.' + base64encode(payload), 'SEVER_SECRET_KEY')
Copy the code

Finally, the signature information is

05dd35b4d20c95430cd1b63406f861de7e4c1476f9dbffa25f30fe08baf8f530
Copy the code

Why is the third part the key? Because the third part can only be generated by the server, and the fundamental reason it can only be generated by the server is that no one knows the signature string SEVER_SECRET_KEY. If someone tampered with only the first and second parts, the server could parse the contents of the server normally, but the third part, which is used as validation, would obviously not match. If someone tampered with all the parts, the server would not be able to parse the third part because SEVER_SECRET_KEY must be different.

The above three parts are combined to form the completed JWT, as shown below:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ewoJJ2lkJzonMTAnLAoJJ2V4cCc6JzIzMDE1OTc0MzMnCn0=.05dd35b4d20c95430cd1b63406f861de7e 4c1476f9dbffa25f30fe08baf8f530Copy the code
1.2 disadvantages of JWT:

1. Security: The payload is only base64URL encoded and not encrypted, so it cannot store sensitive information. At the same time,

JWT should use HTTPS protocol to prevent identity theft if it is disclosed.

2. Performance: JWT is very long in order to achieve security, that is, HTTP request overhead.

1.3 Advantages of JWT:

1. Expansibility: in the case of distributed application deployment, a single token on the client can access all servers, while avoiding multi-machine data sharing of session IDS between servers.

2, performance: JWT information storage, can effectively reduce the number of server query database.

2, Spring Boot + JWT implementation

The details of creating the Spring Boot project and configuring the database are skipped. Below is the structure of the package that will be used in the following project and its description:

Bean: Holds entity objects

Common: Stores common modules such as interceptors

Controller: indicates the control layer

Dto: Objects defined when the control layer receives front-end properties

Mapper: data layer

Service: indicates the service layer

Utils: utility class

You can ignore the hierarchy entirely at this point, because the following examples will have specific application scenarios that are easier to understand.

Prepare for the early stage:

Step 1: Write a utility class JWTUtils that generates and validates JWT (obviously should be placed under utils) :

@Component
public class JWTUtils {

    /** * Generate token */
    public static String getToken(Map<String,String> map) {
        Date date = new Date(System.currentTimeMillis() + 86400*1000);
        Algorithm algorithm = Algorithm.HMAC256("Xinmachong666");
        // Create JWT Builder
        JWTCreator.Builder builder = JWT.create();
        //payload
        map.forEach((k,v)->{
            builder.withClaim(k,v);
        });
        // With username information
        return builder
                // Expiration time
                .withExpiresAt(date)
                // Create a new JWT and mark it with the given algorithm
                .sign(algorithm);
    }

    /** * Check whether the token is correct */
    public static DecodedJWT verify(String token) {
        return JWT.require(Algorithm.HMAC256("Xinmachong666")).build().verify(token);
    }
Copy the code

Where 86400*1000 JWT validity period is set to 1 day, “Xinmachong666” is the previously mentioned very secret signature string.

Step 2: Write a JWT interceptor to validate all JWTS:

public class JWTInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // Get the token in the request header
        ApiResponse apiResponse = null;
        String token = request.getHeader("token");
        try {
            JWTUtils.verify(token);
            return true;
        }catch (SignatureVerificationException e){
            apiResponse = new ApiResponse(505."Invalid signature".false);
        }catch (TokenExpiredException e){
            apiResponse = new ApiResponse(505."The token expired".false);
        }catch (AlgorithmMismatchException e){
            apiResponse = new ApiResponse(505."Inconsistent Token algorithms".false);
        }catch (Exception e){
            apiResponse = new ApiResponse(505."The token is invalid".false);
        }
        String json = new ObjectMapper().writeValueAsString(apiResponse);
        response.setContentType("application/json; charset=UTF-8");
        response.getWriter().println(json);
        return false; }}Copy the code

This class implements the HandlerInterceptor and overrides the implementation part of the preprocessing.

Step 3: Although the JWT validation was preprocessed, the server now does not know how and when to intercept the JWT. For example: JWT verified preprocessing is like the little sister of tollbooths, but now there is no tollbooths, how can you let her charge? To collect tolls with Black Li Kui’s axe? A: So all cars have to be charged? Obviously not a fire truck? Therefore, the third step is to write a configuration class to selectively intercept the configured route, as shown below:

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JWTInterceptor())
                .addPathPatterns("/ * *")
                .excludePathPatterns("/v1/user/login"); }}Copy the code

Where addPathPatterns(“/**”) blocks all routes (stay and buy money if you want to cross it) and excludePathPatterns(“/v1/user/login”) shows routes that can be let through (fire fighter: charge for fighting a fire? Little sister: Mistake mistake).

Part 4: Write a login interface to test, first write the control layer code (obviously the UserController class is in the Controller package) :

@RestController
@RequestMapping("/v1/user")
@Validated
@CrossOrigin
public class UserController {

    @Resource
    private UserService userService;


    @PostMapping("/login")
    public ApiResponse login(@RequestBody @Validated LoginDTO loginDTO) {
        String salt = this.userService.getSaltByAccount(loginDTO);
        String password = new SimpleHash("MD5",loginDTO.getPassword(),salt,1).toHex();
        String realPassword = this.userService.getPasswordByAccount(loginDTO);
        if(StringUtils.isEmpty(realPassword)){
            return new ApiResponse(400."User name error".0);
        } else if(! realPassword.equals(password)) {return new ApiResponse(400."Password error".0);
        } else {
            LoginVO loginVO = this.userService.getUserMsg(loginDTO);
            return new ApiResponse(200."success",loginVO); }}@GetMapping("/test")
    public ApiResponse test(HttpServletRequest request) {
        String username = this.userService.getUsernameByJWT(request);
        return new ApiResponse(200."success",username); }}Copy the code

Where LoginDTO is the object to be received by the control layer:

@Data
@NoArgsConstructor
public class LoginDTO {
    @notblank (message = "user name cannot be blank ")
    private String account;
    @notblank (message = "password cannot be blank ")
    private String password;
}
Copy the code

ApiResponse is a custom return value format:

@Data
public class ApiResponse {

    private int code;
    private String msg;
    private Object data;

    public ApiResponse(int code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data; }}Copy the code

The getSaltByAccount(), getPasswordByAccount(), and getUserMsg() methods are implemented in the business layer as follows (obviously the UserService class goes into the Service package) :

@Service
public class UserService {

    @Resource
    private UserMapper userMapper;
    @Resource
    private GetInformationFromJWT getInformationFromJWT;

    public String getSaltByAccount(LoginDTO loginDTO) {
        QueryWrapper<MeyerUser> wrapper = new QueryWrapper<>();
        wrapper.lambda().eq(MeyerUser::getAccount,loginDTO.getAccount());
        return this.userMapper.selectOne(wrapper).getSalt();
    }

    public String getPasswordByAccount(LoginDTO loginDTO) {
        QueryWrapper<MeyerUser> wrapper = new QueryWrapper<>();
        wrapper.lambda().eq(MeyerUser::getAccount,loginDTO.getAccount());
        return this.userMapper.selectOne(wrapper).getPassword();
    }

    public LoginVO getUserMsg(LoginDTO loginDTO) {
        QueryWrapper<MeyerUser> wrapper = new QueryWrapper<>();
        wrapper.lambda().eq(MeyerUser::getAccount,loginDTO.getAccount());
        MeyerUser meyerUser = this.userMapper.selectOne(wrapper);
        String token = this.getToken(meyerUser);

        LoginVO loginVO = new LoginVO();
        loginVO.setToken(token);
        BeanUtils.copyProperties(meyerUser,loginVO);
        return loginVO;
    }

    public String getToken(MeyerUser meyerUser) {
        Map<String,String> payload = new HashMap<>();
        payload.put("userId", meyerUser.getId()+"");
        payload.put("username", meyerUser.getUsername());
        payload.put("account", meyerUser.getAccount());
        return JWTUtils.getToken(payload);
    }
    
    public String getUsernameByJWT(HttpServletRequest request) {
        return this.getInformationFromJWT.getUsernameByJWT(request); }}Copy the code

Since projects often retrieve information stored in JWT, I extracted this functionality into a new class:

@Component
public class GetInformationFromJWT {

    public int getUserIdByJWT(HttpServletRequest request){
        String token = request.getHeader("token");
        DecodedJWT verify = JWTUtils.verify(token);
        return Integer.parseInt(verify.getClaim("userId").asString());
    }

    public int getRoleIdByJWT(HttpServletRequest request){
        String token = request.getHeader("token");
        DecodedJWT verify = JWTUtils.verify(token);
        return Integer.parseInt(verify.getClaim("roleId").asString());
    }

    public String getUsernameByJWT(HttpServletRequest request){
        String token = request.getHeader("token");
        DecodedJWT verify = JWTUtils.verify(token);
        return verify.getClaim("username").asString(); }}Copy the code

Next are the UserMapper and User entities:

@Repository
public interface UserMapper extends BaseMapper<MeyerUser> {}Copy the code
@Data
@Accessors(chain = true)
public class User {
    @TableId(type = IdType.AUTO)
    private int id;
    private String username;
    private String account;
    private String password;
    private String salt;
    private String staffNo;
    private String nickname;
    private String avatar;
    @JsonIgnore
    private Date createTime;
    @JsonIgnore
    private Date updateTime;
    @JsonIgnore
    @TableLogic
    private Date deleteTime;
}
Copy the code