Spring之拦截器和过滤器

Spring拦截器

拦截器简介

Spring拦截器是一种基于AOP的技术,本质也是使用一种代理技术,它主要作用于接口请求中的控制器,也就是Controller。

因此它可以用于对接口进行权限验证控制。

创建拦截器

创建一个DemoInterceptor类实现HandlerInterceptor接口,重写preHandle(),postHandle(),afterCompletion() 三个方法,如下代码:

@Component
public class DemoInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("在处理器执行之前执行......");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 当处理器发生异常返回时,postHandle() 将不会执行
        System.out.println("在处理器执行之后执行......");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 在处理器执行完成后执行,相当于try catch finally中的finally
        System.out.println("afterCompletion......");
    }
}

创建拦截器之后,我们还需要将其注册到Spring程序中,以便启用它。

注册拦截器

创建一个Spring配置类实现WebMvcConfigurer接口,并重写addInterceptors()方法,用于将拦截器添加到程序中。

@Configuration
public class MvcConfig implements WebMvcConfigurer {

   @Autowired
   private DemoInterceptor demoInterceptor;

   /**
     * 自定义拦截规则
   */
   @Override
   public void addInterceptors(InterceptorRegistry registry) {
      //addPathPatterns 用于添加需要拦截的路径
      //excludePathPatterns 用于排除不需要被拦截的路径
      registry.addInterceptor(demoInterceptor).addPathPatterns("/**");
   }

   /**
   * 此方法用于配置静态资源路径
   **/
   @Override
   public void addResourceHandlers(ResourceHandlerRegistry registry)
   {
       /** 本地文件上传路径 */
       registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**").addResourceLocations("file:" + ProjectConfig.getProfile() + "/");

       /**
        * 使用跨域拦截器时可能会导致Swagger2页面无法访问,需要重新配置静态资源访问
       **/
       registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
       registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
   }

   /**
    * 跨域配置
    */
   @Bean
   public CorsFilter corsFilter()
   {
       CorsConfiguration config = new CorsConfiguration();
       config.setAllowCredentials(true);
       // 设置访问源地址
       config.addAllowedOriginPattern("*");
       // 设置访问源请求头
       config.addAllowedHeader("*");
       // 设置访问源请求方法
       config.addAllowedMethod("*");
       // 有效期 1800秒
       config.setMaxAge(1800L);
       // 添加映射路径,拦截一切请求
       UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
       source.registerCorsConfiguration("/**", config);
       // 返回新的CorsFilter
       return new CorsFilter(source);
   }
}

原理架构图

image

拦截器应用案例

防止重复提交拦截器

@Component
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
{
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {
        if (handler instanceof HandlerMethod)
        {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            // 验证当前访求的方法是否使用 @RepeatSubmit 注解
            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
            if (annotation != null)
            {
                if (this.isRepeatSubmit(request, annotation))
                {
                    AjaxResult ajaxResult = AjaxResult.error(annotation.message());
                    ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
                    return false;
                }
            }
            return true;
        }
        else
        {
            return true;
        }
    }

    /**
     * 验证是否重复提交由子类实现具体的防重复提交的规则
     *
     * @param request
     * @return
     * @throws Exception
     */
    public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
}

这里到自定义注解 @RepeatSubmit

/**
 * 自定义注解防止表单重复提交
 * 
 * @author hviger
 *
 */
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{
    /**
     * 间隔时间(ms),小于此时间视为重复提交
     */
    public int interval() default 5000;

    /**
     * 提示消息
     */
    public String message() default "不允许重复提交,请稍候再试";
}

Spring过滤器

过滤器简介

过滤器 Filter 由 Servlet 提供,基于函数回调实现链式对网络请求响应的拦截与修改。由于基于 Servlet ,其可以对web服务器管理的几乎所有资源进行拦截(JSP、图片文件、HTML 文件、CSS文件等)。

定义一个过滤器,需要实现 javax.servlet.Filter 接口。
Filter 并不是一个 Servlet,它不能直接向客户端生成响应,只是拦截已有的请求,对不需要或不符合的信息资源进行预处理。

过滤器可以定义多个,按照过滤器链顺序调用:
image

概念和作用

概念:过滤器位于客户端和web应用程序之间,用于检查和修改两者之间经过的请求;在请求到达Servlet/JSP之前,用过滤器截获请求;

作用:在客户端的请求访问后端资源之前,拦截这些请求(添加处理)。

场景:实现URL级别的权限访问控制、过滤敏感词汇、压缩响应信息等一些高级功能。

Filter 的生命周期

init(): 初始化Filter 实例,Filter 的生命周期与 Servlet 是相同的,也就是当 Web 容器(tomcat)启动时,调用 init() 方法初始化实例,Filter只会初始化一次。需要设置初始化参数的时候,可以写到init()方法中。
doFilter(): 业务处理,拦截要执行的请求,对请求和响应进行处理,一般需要处理的业务操作都在这个方法中实现
destroy() : 销毁实例,关闭容器时调用 destroy() 销毁 Filter 的实例。

过滤器的使用方式

首先要实现 javax.servlet.Filter 接口,之后将 Filter 声明为 Bean 交由 Spring 容器管理。

传统javaEE增加Filter是在web.xml中配置

<filter>
     <filter-name>TestFilter</filter-name>
        <filter-class>com.cppba.filter.TestFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>TestFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <init-param>
       <param-name>paramName</param-name>
       <param-value>paramValue</param-value>
    </init-param>
</filter-mapping>

方式一:@WebFilter注解

通过 @WebFilter 注解,将类声明为 Bean 过滤器类,在启动类添加注解 @ServletComponentScan ,让 Spring 可以扫描到。

@WebFilter
public class WebVisitFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    /**
     * 输出访问 ip
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        //获取访问 ip 地址
        HttpServletRequest req = (HttpServletRequest) request;
        String visitIp = req.getRemoteAddr();
        visitIp = "0:0:0:0:0:0:0:1".equals(visitIp) ? "127.0.0.1" : visitIp;
        // 每次拦截到请求输出访问 ip
        System.out.println("访问 IP = " + visitIp);
        chain.doFilter(req, response);
    }

    @Override
    public void destroy() {
    }
}

@SpringBootApplication
@ServletComponentScan
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@WebFilter作用:
Tomcat 的 servlet 包下的注解,通过 @WebFilter 注解可以将指定类声明为过滤器。
@WebFilter 属性中没有配置顺序的,其执行顺序和 Filter 类名称字符排序有关,如果需要设置执行顺序,可以在命名的时候注意一下。

方式二:@Component注解

使用 @Component 将类声明为 Bean ,配合使用 @Order 注解可以设置过滤器执行顺序。

@Order(1)
@Component
public class WebVisitFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    /**
     * 输出访问 IP
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        // 业务处理
    }
    @Override
    public void destroy() {
    }
}

方式三:Java Config 配置类

使用 @Configuration + @Bean 配置类,注解声明Bean,交由 Spring 容器管理。
Java Config 的方式可以通过 @Bean 配置顺序或 FilterRegistrationBean.setOrder() 决定 Filter 执行顺序。

public class WebVisitFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        // 业务处理
    }
    @Override
    public void destroy() {
    }
}

@Configuration
public class WebVisitFilterConfig {

    /**
     * 注册 过滤器 Filter
     */
    @Bean
    public FilterRegistrationBean<Filter> webVisitFilterConfigRegistration() {
        //匹配拦截 URL
        String urlPatterns = "/admin/*,/system/*";
        FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<Filter>();
        registration.setDispatcherTypes(DispatcherType.REQUEST);
        registration.setFilter(new WebVisitFilter());
        registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
        //设置名称
        registration.setName("webVisitFilter");
        //设置过滤器链执行顺序
        registration.setOrder(3);
        //启动标识
        registration.setEnabled(true);
        //添加初始化参数
        registration.addInitParameter("enabel", "true");
        return registration;
    }
}

FilterChain 的作用:
过滤器链是一种责任链模式的设计实现,在一个Filter 处理完成业务后,通过 FilterChain 调用过滤器链中的下一个过滤器。

关于OncePerRequestFilter

OncePerRequestFilter:顾名思义,它能够确保在一次请求中只通过一次filter,而需要重复的执行。

此方法是为了兼容不同的web container,也就是说并不是所有的container都是我们期望的只过滤一次,servlet版本不同,执行过程也不同。

简单的说就是去适配了不同的web容器,以及对异步请求,也只过滤一次的需求。

在servlet2.3中,Filter会经过一切请求,包括服务器内部使用的forward转发请求和<%@ include file=”/login.jsp”%>的情况
servlet2.4中的Filter默认情况下只过滤外部提交的请求,forward和include这些内部转发都不会被过滤

因此建议在Spring环境下使用Filter的话,个人建议继承OncePerRequestFilter吧,而不是直接实现Filter接口。这是一个比较稳妥的选择。

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    @Autowired
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
        {
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }

    @Override
    protected void initFilterBean() throws ServletException {
        System.out.println("Filter初始化...");
    }
}

常见应用场景

  • 登录验证
  • 统一编码处理
  • 敏感字符过滤

过滤器链抛出异常处理方式

在过滤器进行拦截操作时,如发生异常,与业务类相同需要捕获异常进行记录并处理。
如果想继续执行业务,可以通过 chain.doFilter(req, response);
对之后的过滤器进行调用。

拦截器和过滤器的区别

过滤器 和 拦截器 均体现了AOP的编程思想,都可以实现诸如日志记录、登录鉴权等功能。

1、实现原理不同

  • 过滤器 是基于函数回调的,doFilter()方法实际上是一个回调接口。
  • 拦截器 则是基于Java的反射机制(动态代理)实现的。

2、使用范围不同

  • 过滤器 实现的是 javax.servlet.Filter 接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter 的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。
  • 拦截器(Interceptor) 它是一个Spring组件,并由Spring容器管理,并不依赖Tomcat等容器,是可以单独使用的。不仅能应用在web程序中,也可以用于Application、Swing等程序中。

3、触发时机不同

  • 过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。
  • 拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。

image

4、拦截的请求范围不同

执行顺序 :Filter 处理中 -> Interceptor 前置 -> controller -> Interceptor 处理中 -> Interceptor 处理后 -> Filter 处理中
由此可见,过滤器Filter执行了两次,拦截器Interceptor只执行了一次。

  • 这是因为过滤器几乎可以对所有进入容器的请求起作用;
  • 而拦截器只会对Controller中请求或访问static目录下的资源请求起作用。

5、注入Bean情况不同

在实际的业务场景中,应用到过滤器或拦截器,为处理业务逻辑难免会引入一些service服务。

  • 过滤器中注入service,发起请求可以正常调用。
  • 在拦截器中注入service,发起请求会报错,debug跟一下会发现注入的service是Null。

这是因为加载顺序导致的问题,拦截器加载的时间点在springcontext之前,而Bean又是由spring进行管理。

拦截器:老子今天要进洞房;
Spring:兄弟别闹,你媳妇我还没生出来呢!

解决方案也很简单,我们在注册拦截器之前,先将Interceptor 手动进行注入。也可以直接在类上使用 @Component 注解注入。

注意:在registry.addInterceptor()注册的是getMyInterceptor() 实例。

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Bean
    public MyInterceptor getMyInterceptor(){
        System.out.println("注入了MyInterceptor");
        return new MyInterceptor();
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(getMyInterceptor()).addPathPatterns("/**");
    }
}

扩展知识

@Component(@Controller、@Service、@Repository)通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中。
@Component和@Bean都是用来注册Bean并装配到Spring容器中,但是Bean比Component的自定义性更强。可以实现一些Component实现不了的自定义加载类。

  • 1、@Component: 注解表明一个类会作为组件类,并告知Spring要为这个类创建bean,使用 @Component注解在一个类上,表示将此类标记为Spring容器中的一个Bean。(相当于创建对象)

  • 2、@Bean是将组件注册到Bean,让IOC容器知道这个组件存在。类上必须加上@Configuration注解。(相当于创建对象)

  • 3、@Autowire:是组件和组件相互调用的时候,自动从ioc中取出来需要用的组件。(调用对象)

6、控制执行顺序不同

实际开发过程中,会出现多个过滤器或拦截器同时存在的情况,不过,有时我们希望某个过滤器或拦截器能优先执行,就涉及到它们的执行顺序。

过滤器用@Order注解控制执行顺序,通过@Order控制过滤器的级别,值越小级别越高越先执行

@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
public class MyFilter implements Filter {}

拦截器默认的执行顺序,就是它的注册顺序,也可以通过Order手动设置控制,值越小越先执行。

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new MyInterceptor3()).addPathPatterns("/**").order(2);
    registry.addInterceptor(new MyInterceptor2()).addPathPatterns("/**").order(1);
    registry.addInterceptor(new MyInterceptor1()).addPathPatterns("/**").order(3);
}

看到输出结果发现,postHandle() 方法被调用的顺序跟 preHandle() 居然是相反的!

如果实际开发中严格要求执行顺序,那就需要特别注意这一点。

Interceptor2 前置
Interceptor3 前置
Interceptor1 前置

controller

Interceptor1 处理中
Interceptor3 处理中
Interceptor2 处理中

Interceptor1 处理后
Interceptor3 处理后
Interceptor2 处理后

拦截器和过滤器的应用场景

image

如果一个请求在经过Spring框架处理之前就要处理(或者说能够/可以被处理),那么这种情况一般选择用过滤器。

  • 对请求URL做限制,限制某些URL的请求不被接受,这个动作是没有必要经过Spring的,直接过滤器初始化规则过滤即可;
  • 对请求数据做字符转换,请求的是密文,转换成明文,顺便再校验一下数据格式,这个也不需要经过Spring;

总之,与详细业务不相关的请求处理都可以用过滤器来做;而与业务相关的自然就用拦截器来做

  • 对业务处理前后的数据做日志的记录;
posted @ 2022-09-23 11:11  盗梦笔记  阅读(271)  评论(0编辑  收藏  举报