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