SpringBoot, uniapp projects integrate token verification

SpringBoot, uniapp projects integrate token verification

Since the front-end uniapp of this small project uses tokens for verification, I also learned how to use related tokens when writing the back-end.

What is a token?

A simple understanding is that the token is a string generated on the server and passed to the client as the token carried in the request. When the user logs in for the first time, the server will generate a token and return it to the client. After that, the client's request will carry this token. The server parsing will intercept these requests and determine whether the token is valid. If there is no token token or it is invalid will be blocked.

Front-end configuration

First, configure the grid interceptor on the front end, and bring a token to the request for authentication on the back end. The front end uses the uView framework. After installing and configuring uView, the following is the interceptor interceptor, which is introduced in mian.js

module.exports = (vm) => {
      // Initialize request configuration
      uni.$u.http.setConfig((config) => {
          /* config Configured globally by default*/
          config.baseURL = 'http://192.168.194.251'; /* root domain name */
          return config
      })
  	
  	// Request interception, so that the request carries the token
  	uni.$u.http.interceptors.request.use((config) => { 
      const token = uni.getStorageSync("token");
      config.header.Authorization = "Bearer " + token;
      config.header.Accept = "application/json";
      return config;
  	})
  	
  	//Response interception
  	uni.$u.http.interceptors.response.use((response) => {
      console.log(response)
      if (response.statusCode != 200) {
        console.log('Response interception succeeded!=200')
      	return response;
      } else {
        console.log('Response interception returns')
      	return response;
      }
    }, (response) => { 
        console.log('This is the return in response to the error')
        console.log(response)
    		return response
    	})
  }
//Introduce interceptor
require('@/common/http.interceptor.js')(app)

Backend configuration

pom import

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

application.yml

audience:
  clientId: 098f6bcd4621d373cade4e832627b4f6
   # The key, encrypted with Base64, can be replaced by itself.  Base64 encryption and decryption tool: http://tool.chinaz.com/Tools/Base64.aspx
  base64Secret: eXViYW9aSk5VU0NFTkVSWVYxLjA=
  # The issuing body of the JWT is stored in the issuer
  iss: issued by yubao
  # Expiration time in milliseconds
  expiresSecond: 604800000

Audience

Create a new entity class for configuration information to obtain the JWT configuration:

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@ConfigurationProperties(prefix = "audience")
@Component
public class Audience {
    //Represents the receiving object of this JWT and stores it in audience
    private String aud;
    private String base64Secret;
    //The issuing body of the JWT is stored in the issuer
    private String iss;
    private int expiresSecond;

}

Create JWT tool class

package com.yubao.zjnu_demo.utils;


import com.yubao.zjnu_demo.common.CustomException;
import com.yubao.zjnu_demo.entity.Audience;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Base64Utils;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
@Slf4j
public class JwtTokenUtil {

    public static final String AUTH_HEADER_KEY = "Authorization";

    public static final String TOKEN_PREFIX = "Bearer ";

    /**
     * Parse jwt
     *
     * @param jsonWebToken
     * @param base64Security
     * @return
     */
    public static Claims parseJWT(String jsonWebToken, String base64Security) {
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
                    .parseClaimsJws(jsonWebToken).getBody();
            return claims;
        } catch (ExpiredJwtException eje) {
            log.error("===== Token Expired =====", eje);
            throw new CustomException("PERMISSION_TOKEN_EXPIRED");
        } catch (Exception e) {
            log.error("===== token Parse exception =====", e);
            throw new CustomException("PERMISSION_TOKEN_INVALID");
        }
    }

    /**
     * build jwt
     *
     * @param userId
     * @param username
     * @param audience
     * @return
     */
    public static String createJWT(String userId, String username, Audience audience) {
        try {
            // Use HS256 encryption algorithm
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

            long nowMillis = System.currentTimeMillis();
            Date now = new Date(nowMillis);

            //Generate signing key
            byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(audience.getBase64Secret());
            Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

            //Add parameters that make up the JWT
            JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
                    // You can put basically unimportant object information in claims
                    .claim("userId", userId)
                    .setSubject(username)           // Represents the principal of this JWT, i.e. its owner
                    .setIssuer(audience.getIss())              // Represents the issuing body of this JWT;
                    .setIssuedAt(new Date())        // is a timestamp representing the issuance time of this JWT;
                    .setAudience(audience.getAud())          // Represents the receiver object of this JWT;
                    .signWith(signatureAlgorithm, signingKey);
            //Add Token expiration time
            int TTLMillis = audience.getExpiresSecond();
            if (TTLMillis >= 0) {
                long expMillis = nowMillis + TTLMillis;
                Date exp = new Date(expMillis);
                builder.setExpiration(exp)  // is a timestamp representing the expiration time of this JWT;
                        .setNotBefore(now); // It is a timestamp representing the start time when the JWT takes effect, which means that validating the JWT before this time will fail
            }

            //Generate JWT
            return builder.compact();
        } catch (Exception e) {
            log.error("Signing failed", e);
            throw new CustomException("PERMISSION_SIGNATURE_ERROR");
        }
    }

    /**
     * Get username from token
     *
     * @param token
     * @param base64Security
     * @return
     */
    public static String getUsername(String token, String base64Security) {
        return parseJWT(token, base64Security).getSubject();
    }

    /**
     * Get user ID from token
     *
     * @param token
     * @param base64Security
     * @return
     */
    public static String getUserId(String token, String base64Security) {
        Claims claims= parseJWT(token,base64Security);
        String userId = (String)claims.get("userId");
        return userId;
    }

    /**
     * Has it expired
     *
     * @param token
     * @param base64Security
     * @return
     */
    public static boolean isExpiration(String token, String base64Security) {
        return parseJWT(token, base64Security).getExpiration().before(new Date());
    }
}

interceptor

Intercept the request from the front end to verify whether it carries the token and whether the token is valid

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * token Validation Interceptor
 */
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private Audience audience;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // Ignore requests annotated with JwtIgnore, and do not perform subsequent token authentication verification
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            JwtIgnore jwtIgnore = handlerMethod.getMethodAnnotation(JwtIgnore.class);
            if (jwtIgnore != null) {
                return true;
            }
        }

        if (HttpMethod.OPTIONS.equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return true;
        }

        // Get request header information authorization information
        final String authHeader = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);
        log.info("## authHeader= {}", authHeader);

        if (StringUtils.isBlank(authHeader) || !authHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX)) {
            log.info("### User is not logged in, please log in first ###");
            throw new CustomException("USER_NOT_LOGGED_IN");
        }

        // get token
        final String token = authHeader.substring(7);

        if (audience == null) {
            BeanFactory factory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
            audience = (Audience) factory.getBean("audience");
        }

        // Verify whether the token is valid--invalid exception has been thrown, and the corresponding information will be returned after the global exception is handled
        JwtTokenUtil.parseJWT(token, audience.getBase64Secret());

        return true;
    }

}

Write the JwtIgnore annotation

Ignore the request with JwtIgnore, and will not do subsequent token verification

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JwtIgnore {
    boolean required() default true;
}

Configure the interceptor

Configure the interceptor in the configuration class

@Override
public void addInterceptors(InterceptorRegistry registry) {
    //The interception path can be configured with multiple available , separated by
    registry.addInterceptor(new JwtInterceptor()).addPathPatterns("/**");
}

/**
 * Cross domain support
 *
 * @param registry
 */
@Override
public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
            .allowedOriginPatterns("*")
            .allowCredentials(true)
            .allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
            .maxAge(3600 * 24);
}

User login interface

//User login
@PostMapping("/login")
@JwtIgnore
public R<JSONObject> loginUser(User user, HttpServletResponse response){
    LambdaQueryWrapper<User> queryWrapper =new LambdaQueryWrapper<>();
    User loginUser=null;
    if(user.getName()!=null){
        String password= DigestUtils.md5Hex(user.getPassword());
        user.setPassword(password);
        queryWrapper.eq(User::getName,user.getName());
        queryWrapper.eq(User::getPassword,user.getPassword());
        loginUser=userService.getOne(queryWrapper);
    }else{
        Object codeInSession=stringRedisTemplate.opsForValue().get(user.getPhone());
        if(codeInSession!=null&&codeInSession.equals(user.getVerifiableCode())) {
            queryWrapper.eq(User::getPhone, user.getPhone());
            loginUser = userService.getOne(queryWrapper);
        }else{
            return R.error("failed to login");
        }
    }
    if(loginUser==null){
        return R.error("Login failed");
    }

    // create token
    String token = JwtTokenUtil.createJWT(loginUser.getId().toString(), loginUser.getName(), audience);
    log.info("### login successful, token={} ###", token);

    // Put the token in the response header
    response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, JwtTokenUtil.TOKEN_PREFIX + token);
    // Response token to client
    JSONObject result = new JSONObject();
    result.put("token", token);

    return R.success(result);
}

In this way, we pass this token to the front end, and the front end stores the token, which is carried in the interceptor every time we request

uni.setStorageSync('token', token)

Processing of requests with token s

In many interfaces, we need to obtain the current user's id, name, etc., but these information are not carried in the request, we can only parse from the token, how to elegantly parse the token information in the required interface?

We use the method parameter parser, refer to this article

Spring provides a variety of resolvers Resolver, such as the commonly used HandlerExceptionResolver that handles exceptions uniformly. At the same time, a resolver HandlerMethodArgumentResolver for handling method parameters is also provided. It contains 2 methods: supportsParameter and resolveArgument. The former is used to judge whether a certain condition is met, and when the condition is met (returns true), the resolveArgument method can be entered for specific processing operations.

Based on HandlerExceptionResolver, we can implement it in the following parts:

  • Custom annotation @CurrentUser for the User parameter on the Controller method;
  • Customize the LoginUserHandlerMethodArgumentResolver, implement the HandlerMethodArgumentResolver interface, check the eligible parameters through the supportsParameter, convert the token into a User object through the resolveArgument method, and assign it to the parameter.
  • Register HandlerMethodArgumentResolver to MVC.

Let's look at the specific implementation, first define the annotation @CurrentUser:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}

Annotations are used for identification, and the parameters specified by the identification need to be processed. The parameters annotated with @CurrentUser are judged and processed by a custom LoginUserHandlerMethodArgumentResolver:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Component
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

    @Autowired
    private Audience audience;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(CurrentUser.class) &&
                parameter.getParameterType().isAssignableFrom(User.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
                                  NativeWebRequest request, WebDataBinderFactory factory) {

        final String authHeader = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);
        // get token
        final String token = authHeader.substring(7);

        Long userId = Long.valueOf(JwtTokenUtil.getUserId(token,audience.getBase64Secret()));
        String name=JwtTokenUtil.getUsername(token,audience.getBase64Secret());
        // TODO obtains User information based on userId, which is omitted here and creates a User object directly.
        User user = new User();
        user.setName(name);
        user.setId(userId);
        return user;
    }
}

In the supportsParameter method, two conditions are used to filter parameters. First, the parameter needs to be annotated with CurrentUser, and the parameter type is User. Returns true when the condition is met, and enters resolveArgument for processing.

In resolveArgument, the token is obtained from the header, and then the corresponding User information is obtained according to the token. Here, UserService can be injected to obtain more user information, and then the constructed User object is returned. In this way, the returned User can be bound to the parameters in the Controller later.

But at this time the custom Resolver does not take effect and needs to be added to MVC:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Resource
    private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(loginUserHandlerMethodArgumentResolver);
    }

}

At this point, you can use this annotation in the Controller to obtain user information. The specific usage is as follows:

public R<FeedDto> getById(@PathVariable Long id,@CurrentUser User currentUser){

Add the annotation CurrentUser before the parameter, the user parameter is the currently logged in user.

Tags: Java Front-end Spring Boot uni-app

Posted by satyac46 on Wed, 05 Oct 2022 05:22:44 +0300