什么是分布式锁
前言
在系统中,我们经常需要在处理用户请求之前和之后执行一些行为,例如检测用户的权限,或者将请求的信息记录到日志中。当然不仅仅这些,所以需要一种机制,拦截用户的请求,在请求的前后添加处理逻辑。
本文就以登录拦截为主,介绍三种方式实现登录拦截。
方案一(过滤器)
过滤器(Filter)是对数据进行过滤,预处理。开发人员可以对客户端提交的数据进行过滤处理,比如敏感词,也可以对服务端返回的数据进行处理。还有就是可以验证用户的登录情况,权限验证,对静态资源进行访问控制,没有登录或者是没有权限时是不能让用户直接访问这些资源的。类似的过滤器还有很多的功能,比如说编码,压缩服务端给客户端返回的各种数据等等。
使用过滤器实现登录拦截要先实现一个过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| public class MyFilter implements Filter {
private static final String CURRENT_USER = "current_user";
protected static List<Pattern> patterns = new ArrayList<Pattern>();
static { patterns.add(Pattern.compile("/index")); patterns.add(Pattern.compile("/login")); patterns.add(Pattern.compile("/register")); }
@Override public void init(FilterConfig filterConfig) throws ServletException {
}
@Override public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; HttpServletResponseWrapper wrapper = new HttpServletResponseWrapper(httpResponse);
String url = httpRequest.getRequestURI().substring(httpRequest.getContextPath().length()); if (isInclude(url)) { filterChain.doFilter(httpRequest, httpResponse); return; } if (SessionUtils.getSessionAttribute(CURRENT_USER) != null) { filterChain.doFilter(httpRequest, httpResponse); return; } else { wrapper.sendRedirect("/index"); }
}
@Override public void destroy() {
}
private boolean isInclude(String url) { for (Pattern pattern : patterns) { Matcher matcher = pattern.matcher(url); if (matcher.matches()) { return true; } } return false; } }
|
然后注册过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Configuration public class WebConfig {
@Bean public FilterRegistrationBean someFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(myFilter()); registration.addUrlPatterns("/*"); registration.setName("myFilter"); return registration; }
@Bean(name = "myFilter") public Filter myFilter() { return new MyFilter(); } }
|
方案二(拦截器)
拦截器(Interceptor)是SpringMVC提供的一种拦截机制,基于AOP动态代理实现,用于请求的预处理和后处理。在SpringMVC中定义一个拦截器有两种方法:第一种是实现HandlerInterceptor
接口,或者继承实现了HandlerInterceptor
接口的类(例如:HandlerInterceptorAdapter
);第二种方法时实现Spring的WebRequestInterceptor
接口,或者继承实现了WebRequestInterceptor
接口的类。这些拦截器都是在Handler的执行周期内进行拦截操作的。下面主要介绍第一种方法。
首先我们要实现HandlerInterceptor
接口定义登录校验的方法。在实现这个接口之前我们要先看看这个接口的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package org.springframework.web.servlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.lang.Nullable; public interface HandlerInterceptor { default boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } default void postHandle( HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { } default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { } }
|
可以看到HandlerInterceptor
接口中有三个要实现的方法,分别是preHandle
、postHandle
、afterCompletion
。
preHandle()
:这个方法将在请求处理之前进行调用。注意:如果该方法的返回值为false
,将视为当前请求结束,不仅自身的拦截器会失效,还会导致其他的拦截器也不再执行。
postHandle()
:只有在 preHandle()
方法返回值为true
时才会执行。会在Controller 中的方法调用之后,DispatcherServlet 返回渲染视图之前被调用。 有意思的是:postHandle()
方法被调用的顺序跟 preHandle()
是相反的,先声明的拦截器 preHandle()
方法先执行,而postHandle()
方法反而会后执行。
afterCompletion()
:只有在 preHandle()
方法返回值为true
时才会执行。在整个请求结束之后, DispatcherServlet 渲染了对应的视图之后执行。
而我们如果要实现登录拦截,就需要重写preHandle()
方法,然后在其中实现登录拦截的逻辑代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| @Component public class LoginInterception implements HandlerInterceptor {
@Resource private RedisTemplate<String, Object> redisTemplate;
@Override public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader(Constant.TOKEN_HEADER_NAME); if (StringUtils.isBlank(token)) { returnNoLogin(response); return false; } User user = (User) redisTemplate.opsForValue().get(Constant.REDIS_USER_PREFIX + token); if (user == null) { returnNoLogin(response); return false; } redisTemplate.expire(Constant.REDIS_USER_PREFIX + token, 30, TimeUnit.MINUTES); return true; }
private void returnNoLogin(HttpServletResponse response) throws IOException { ServletOutputStream outputStream = response.getOutputStream(); response.setStatus(401); response.setContentType("Application/json;charset=utf-8"); Result<String> result = Result.<String>builder() .code(HttpStatus.UNAUTHORIZED.value()) .errorMsg("未登陆,请先登陆") .build(); String resultString = JSONUtil.toJsonStr(result); outputStream.write(resultString.getBytes(StandardCharsets.UTF_8)); }
}
|
然后配置WebMvcConfig配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @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()); }
}
|
方案三(AOP+自定义注解)
通过自定义注解和AOP也可以实现登录拦截的功能
首先自定义注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Mapping public @interface GlobalInterceptor {
boolean checkLogin() default true;
boolean checkAdmin() default false; }
|
然后编写自定义注解的切面类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
|
@Component("globalOperationAspect") @Aspect public class GlobalOperationAspect {
@Resource private RedisUtils redisUtils;
private static final Logger logger = LoggerFactory.getLogger(GlobalOperationAspect.class);
@Before("@annotation(com.easychat.annotation.GlobalInterceptor)") public void interceptorDo(JoinPoint joinPoint) { try { Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); GlobalInterceptor interceptor = method.getAnnotation(GlobalInterceptor.class); if (null == interceptor) { return; } if (interceptor.checkLogin() || interceptor.checkAdmin()) { checkLogin(interceptor.checkAdmin()); } } catch (BusinessException e) { logger.error("全局拦截器异常", e); throw e; } catch (Throwable e) { logger.error("全局拦截器异常", e); throw new BusinessException(ResponseCodeEnum.CODE_500); } }
private void checkLogin(Boolean checkAdmin) { HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); String token = request.getHeader("token"); TokenUserInfoDto tokenUserInfoDto = (TokenUserInfoDto) redisUtils.get(Constants.REDIS_KEY_WS_TOKEN + token); if (tokenUserInfoDto == null) { throw new BusinessException(ResponseCodeEnum.CODE_901); } if (checkAdmin && !tokenUserInfoDto.getAdmin()) { throw new BusinessException(ResponseCodeEnum.CODE_404); } } }
|
最后在需要登录拦截的Controller方法上使用@GlobalInterceptor
注解即可实现登录拦截。
总结
以上就是本文关节登录拦截的三种实现方式,而这三种方式的执行顺序是 Filter > Interceptor > AOP 。而且拦截器和过滤器的区别可参见面试突击90:过滤器和拦截器有什么区别?-腾讯云开发者社区-腾讯云 (tencent.com)
Reference:
java过滤器实现登录拦截处理_java 服务端屏蔽某个接口-CSDN博客
SpringBoot自定义注解—AOP方式和拦截器方式实现_自定义注解aop拦截-CSDN博客
Springboot实现登录拦截的三种方式_springboot登录拦截-CSDN博客