Three ways to implement login interception in Springboot

Posted by roseplant on Mon, 03 Jan 2022 12:31:17 +0100

1. Login authentication

1.1 INTRODUCTION

In the current front-end and back-end projects, without using the framework, after successful login, the production Token will be sent to the front end, and each request will be carried to the background through a cookie or request header. Before executing the business code, the background will first verify whether the user logs in and obtain the permission of the interface according to the login status. This operation is expected to be separated from business code to realize non-invasive login interception and permission control.

1.2 method

spring provides the following three ways to realize non-invasive login and permission verification, which are described one by one below

  • Filter provided in Java Web
  • Interceptor provided in spring MVC
  • AOP technology provided by Spring + custom annotation

1.3 expansion

After the login interception is implemented by using the above three methods, the corresponding JSON error data will be directly for login. However, if you want to use the login information stored by the login user in the method, you have to retrieve it. Two simple methods are recommended

  • After determining the login status in the interceptor, it is stored in the thread pool object ThreadLocal object. But if it is not in a thread, it is more troublesome.
  • Using the user-defined parameter parser provided by SpringMvc, combined with the user-defined parameter annotation, complete the automatic injection of the annotated parameters. Relatively simple, recommended

2. Realization

Source code address of this article: https://gitee.com/he_linhai/spring-boot-csdn/tree/master/01-spring-boot-auth-filter

pom.xml

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.5.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <!-- springboot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
    </dependency>
    <!-- aop -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <!-- redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    <!-- servlet -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
    </dependency>
    <!-- Other Toolkits -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.6.0</version>
    </dependency>
</dependencies>

2.1 project structure and pre preparation

  • Front implementation, login logic, which provides three interfaces through UserController, login, query user and test interface

    After successful login, the login interface generates a token and uses UUID. The encryption algorithm is not used here. Store the corresponding relationship between token and login information in redis, and the expiration time is half an hour.

  • test

    PostMan is used for interface test here

Login login interface

The post /user/login request is successful and a token is returned

findAllUser query interface

get /user returns the list of users

2.2 the filter realizes login interception

LoginFilter login filter

public class LoginFilter implements Filter {

    private final RedisTemplate<String, Object> redisTemplate;
    private final LoginProperties loginProperties;

    public LoginFilter(RedisTemplate<String, Object> redisTemplate, LoginProperties loginProperties) {
        this.redisTemplate = redisTemplate;
        this.loginProperties = loginProperties;
    }

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        // Filter path
        String requestURI = httpServletRequest.getRequestURI();
        if (!loginProperties.getFilterExcludeUrl().contains(requestURI)) {
            // Get token
            String token = httpServletRequest.getHeader(Constant.TOKEN_HEADER_NAME);
            if (StringUtils.isBlank(token)) {
                returnNoLogin(response);
                return;
            }
            // Get the user corresponding to the token from redis
            User user = (User) redisTemplate.opsForValue().get(Constant.REDIS_USER_PREFIX + token);
            if (user == null) {
                returnNoLogin(response);
                return;
            }
            // token renewal
            redisTemplate.expire(Constant.REDIS_USER_PREFIX + token, 30, TimeUnit.MINUTES);
        }
        chain.doFilter(request, response);
    }

    /**
     * Return the error message of not logging in
     * @param response ServletResponse
     */
    private void returnNoLogin(ServletResponse response) throws IOException {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        ServletOutputStream outputStream = httpServletResponse.getOutputStream();
        // Set return 401 and response code
        httpServletResponse.setStatus(401);
        httpServletResponse.setContentType("Application/json;charset=utf-8");
        // Construct return response body
        Result<String> result = Result.<String>builder()
                .code(HttpStatus.UNAUTHORIZED.value())
                .errorMsg("Not logged in, please log in first")
                .build();
        String resultString = JSONUtil.toJsonStr(result);
        outputStream.write(resultString.getBytes(StandardCharsets.UTF_8));
    }

    @Override
    public void destroy() {
    }

}

WebMvcConfig configuration interceptor

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Resource
    private LoginProperties loginProperties;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * Add login filter
     */
    @Bean
    public FilterRegistrationBean<Filter> loginFilterRegistration() {
        // Register LoginFilter
        FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new LoginFilter(redisTemplate, loginProperties));
        // Set name
        registrationBean.setName("loginFilter");
        // Set interception path
        registrationBean.addUrlPatterns(loginProperties.getFilterIncludeUrl().toArray(new String[0]));
        // Specify the order. The smaller the number, the higher the number
        registrationBean.setOrder(-1);
        return registrationBean;
    }

}

test

  • If you do not log in to the query interface, an error 401 will be reported
  • Normal access after login

2.3 the interceptor realizes login interception

Logininception login interceptor

@Component
public class LoginInterception implements HandlerInterceptor {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // Get token
        String token = request.getHeader(Constant.TOKEN_HEADER_NAME);
        if (StringUtils.isBlank(token)) {
            returnNoLogin(response);
            return false;
        }
        // Get the user corresponding to the token from redis
        User user = (User) redisTemplate.opsForValue().get(Constant.REDIS_USER_PREFIX + token);
        if (user == null) {
            returnNoLogin(response);
            return false;
        }
        // token renewal
        redisTemplate.expire(Constant.REDIS_USER_PREFIX + token, 30, TimeUnit.MINUTES);
        // Release
        return true;
    }

    /**
     * Return the error message of not logging in
     * @param response ServletResponse
     */
    private void returnNoLogin(HttpServletResponse response) throws IOException {
        ServletOutputStream outputStream = response.getOutputStream();
        // Set return 401 and response code
        response.setStatus(401);
        response.setContentType("Application/json;charset=utf-8");
        // Construct return response body
        Result<String> result = Result.<String>builder()
                .code(HttpStatus.UNAUTHORIZED.value())
                .errorMsg("Not logged in, please log in first")
                .build();
        String resultString = JSONUtil.toJsonStr(result);
        outputStream.write(resultString.getBytes(StandardCharsets.UTF_8));
    }

}

WebMvcConfig configuration interceptor

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Resource
    private LoginProperties loginProperties;
    @Resource
    private LoginInterception loginInterception;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterception)
                .addPathPatterns(loginProperties.getInterceptorIncludeUrl())
                .excludePathPatterns(loginProperties.getInterceptorExcludeUrl());
    }

}

test

  • The access interface is not logged in and is intercepted normally
  • Login access interface, normal traffic

2.4 AOP + custom annotation implementation

LoginValidator custom annotation

/**
 * @description Login verification annotation, user aop verification
 * @author HLH
 * @email 17703595860@163.com
 * @date Created in 2021/8/1 9:35 PM
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoginValidator {

    boolean validated() default true;

}

LoginAspect login AOP class

@Component
@Aspect
public class LoginAspect {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * Pointcuts, annotated methods or annotated classes
     *  Intercepts methods annotated on classes or methods
     */
    @Pointcut(value = "@annotation(xyz.hlh.annotition.LoginValidator) || @within(xyz.hlh.annotition.LoginValidator)")
    public void pointCut() {}

    @Around("pointCut()")
    public Object before(ProceedingJoinPoint joinpoint) throws Throwable {
        // Gets the LoginValidator annotation on the method
        MethodSignature methodSignature = (MethodSignature)joinpoint.getSignature();
        Method method = methodSignature.getMethod();
        LoginValidator loginValidator = method.getAnnotation(LoginValidator.class);
        // If yes, and the value is false, no verification is performed
        if (loginValidator != null && !loginValidator.validated()) {
            return joinpoint.proceed(joinpoint.getArgs());
        }
        // Get request and response through normal verification
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null || requestAttributes.getResponse() == null) {
            // If it is not from the previous paragraph and there is no request, it will be released directly
            return joinpoint.proceed(joinpoint.getArgs());
        }
        HttpServletRequest request = requestAttributes.getRequest();
        HttpServletResponse response = requestAttributes.getResponse();
        // Get token
        String token = request.getHeader(Constant.TOKEN_HEADER_NAME);
        if (StringUtils.isBlank(token)) {
            returnNoLogin(response);
            return null;
        }
        // Get the user corresponding to the token from redis
        User user = (User) redisTemplate.opsForValue().get(Constant.REDIS_USER_PREFIX + token);
        if (user == null) {
            returnNoLogin(response);
            return null;
        }
        // token renewal
        redisTemplate.expire(Constant.REDIS_USER_PREFIX + token, 30, TimeUnit.MINUTES);
        // Release
        return joinpoint.proceed(joinpoint.getArgs());
    }

    /**
     * Return the error message of not logging in
     * @param response ServletResponse
     */
    private void returnNoLogin(HttpServletResponse response) throws IOException {
        ServletOutputStream outputStream = response.getOutputStream();
        // Set return 401 and response code
        response.setStatus(401);
        response.setContentType("Application/json;charset=utf-8");
        // Construct return response body
        Result<String> result = Result.<String>builder()
                .code(HttpStatus.UNAUTHORIZED.value())
                .errorMsg("Not logged in, please log in first")
                .build();
        String resultString = JSONUtil.toJsonStr(result);
        outputStream.write(resultString.getBytes(StandardCharsets.UTF_8));
    }

}

Controller annotation

test

  • The access interface is not logged in and is intercepted normally
  • Login access interface, normal traffic

2.5 sequence analysis

If both filter interceptor and AOP are available, the order is as follows

  • Filter
  • Interceptor
  • AOP

3. Expansion

3.1 ThreadLocal stores the login user

LoginUserThread thread object

public class LoginUserThread {

    /** Thread pool variable */
    private static final ThreadLocal<User> LOGIN_USER = new ThreadLocal<>();

    private LoginUserThread() {}

    public static User get() {
        return LOGIN_USER.get();
    }
    
    public void put(User user) {
        LOGIN_USER.set(user);
    }

    public void remove() {
        LOGIN_USER.remove();
    }

}

LoginInterceptor transforms to put the thread object in the pre method and clear the pre object in after

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // Get token
    String token = request.getHeader(Constant.TOKEN_HEADER_NAME);
    if (StringUtils.isBlank(token)) {
        returnNoLogin(response);
        return false;
    }
    // Get the user corresponding to the token from redis
    User user = (User) redisTemplate.opsForValue().get(Constant.REDIS_USER_PREFIX + token);
    if (user == null) {
        returnNoLogin(response);
        return false;
    }
    // Store such as ThreadLocal
    LoginUserThread.put(user);
    // Release
    return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    // Store such as ThreadLocal
    LoginUserThread.remove();
}

test

The method is modified as follows

@GetMapping
public ResponseEntity<?> findAllUser() {
    System.out.println(LoginUserThread.get());
    return success(PRE_USER_LIST);
}

Visit to view the console print results

3.2 spring MVC parameter parser

LoginUser custom annotation

/**
 * @description The login Parameter annotation is parsed by the spring parameter parser
 * @author HLH
 * @email 17703595860@163.com
 * @date Created in 2021/8/1 9:35 PM
 */
@Target(ElementType.PARAMETER)  // Action on parameter
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoginUser {

}

LoginUserResolver parameter parser

/**
 * @description Login parameter injection and parsing through spring parameter parser
 * @author HLH
 * @email 17703595860@163.com
 * @date Created in 2021/8/1 9:35 PM
 */
@Component
public class LoginUserResolver implements HandlerMethodArgumentResolver {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * Whether to intercept
     * @param parameter Parameter object
     * @return true,Intercept. false, do not intercept
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(LoginUser.class);
    }

    /**
     * Method executed after interception
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        // Get the token from the request. Here, only parameter analysis is performed, and login verification is not performed
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            return null;
        }
        HttpServletRequest request = requestAttributes.getRequest();
        // Get token
        String token = request.getHeader(Constant.TOKEN_HEADER_NAME);
        if (StringUtils.isBlank(token)) {
            return null;
        }
        // Get the user corresponding to the token from redis
        return (User) redisTemplate.opsForValue().get(Constant.REDIS_USER_PREFIX + token);
    }
}

WebMvcConfig add parameter parser

@Resource
private LoginUserResolver loginUserResolver;

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

test

controller method transformation

@GetMapping("/test")
public String test(@LoginUser User user) {
    System.out.println(user);
    return "Test code";
}

Access to view console results

Topics: Spring Boot