Spring实用系列-深入了解SpringMVC OncePerRequestFilter过滤器原理

导语
  OncePerRequestFilter作为SpringMVC中的一个过滤器,在每次请求的时候都会执行。它是对于Filter的抽象实现。比起特定的过滤器来说,这个过滤器对于每次的请求都进行请求过滤。下面就来分析OncePerRequestFilter

OncePerRequestFilter介绍

   Filter 拦截器也叫做过滤器,它的作用就是帮助拦截一些用户请求,并对用户请求做一些初步的处理工作全局被初始化一次,这里介绍的OncePerRequestFilter,网上很多的资料都是说它只会拦截一次请求,这里需要更正一点,就是OncePerRequestFilter表示每次的Request都会进行拦截,不管是资源的请求还是数据的请求。有兴趣的可以了解一下Servlet相关的知识。这里主要是对OncePerRequestFilter的说明。

org.springframework.web.filter.OncePerRequestFilter

在这里插入图片描述
  从上图中可以看到OncePerRequestFilter存在于spring-web模块中,也就是它是Spring框架对于Web Servlet的封装。并且可以看到Spring MVC提供很多的Filter过滤器。其实这些Filter的实现都是大同小异的。下面先来看看Filter接口。

Filter接口 介绍

  几乎所有的Filter都是这个接口的实现,对于一个接口来讲就是定义一个规则,接下来它的实现类都是扩展这些规则,完成一些自定义的Filter开发。

public interface Filter {

    /** 
     * Called by the web container to indicate to a filter that it is
     * being placed into service.
     *
     * <p>The servlet container calls the init
     * method exactly once after instantiating the filter. The init
     * method must complete successfully before the filter is asked to do any
     * filtering work.
     * 
     * <p>The web container cannot place the filter into service if the init
     * method either
     * <ol>
     * <li>Throws a ServletException
     * <li>Does not return within a time period defined by the web container
     * </ol>
     */
    public void init(FilterConfig filterConfig) throws ServletException;
	
	
    /**
     * The <code>doFilter</code> method of the Filter is called by the
     * container each time a request/response pair is passed through the
     * chain due to a client request for a resource at the end of the chain.
     * The FilterChain passed in to this method allows the Filter to pass
     * on the request and response to the next entity in the chain.
     *
     * <p>A typical implementation of this method would follow the following
     * pattern:
     * <ol>
     * <li>Examine the request
     * <li>Optionally wrap the request object with a custom implementation to
     * filter content or headers for input filtering
     * <li>Optionally wrap the response object with a custom implementation to
     * filter content or headers for output filtering
     * <li>
     * <ul>
     * <li><strong>Either</strong> invoke the next entity in the chain
     * using the FilterChain object
     * (<code>chain.doFilter()</code>),
     * <li><strong>or</strong> not pass on the request/response pair to
     * the next entity in the filter chain to
     * block the request processing
     * </ul>
     * <li>Directly set headers on the response after invocation of the
     * next entity in the filter chain.
     * </ol>
     */
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain)
            throws IOException, ServletException;


    /**
     * Called by the web container to indicate to a filter that it is being
     * taken out of service.
     *
     * <p>This method is only called once all threads within the filter's
     * doFilter method have exited or after a timeout period has passed.
     * After the web container calls this method, it will not call the
     * doFilter method again on this instance of the filter.
     *
     * <p>This method gives the filter an opportunity to clean up any
     * resources that are being held (for example, memory, file handles,
     * threads) and make sure that any persistent state is synchronized
     * with the filter's current state in memory.
     */
    public void destroy();
}

  从上面的描述中可以看到,这个接口定义了三个方法,也就是说凡是继承这个接口的类都要实现这三个方法,对于这三个方法而言,最重要的就是doFilter 方法,也就是在实现自定义的Filter的时候最为主要的就是如何去实现这个方法。那么既然OncePerRequestFilter作为它的实现类下面就来看看OncePerRequestFilter是如何实现这个方法

  public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain)
            throws IOException, ServletException;

OncePerRequestFilter类继承关系

在这里插入图片描述
  结合上面的内容来看一下OncePerRequestFilter是如何实现doFilter()

@Override
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
	//首先判断Request是否是一个HttpServletRequest的请求
	if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
			throw new ServletException("OncePerRequestFilter just supports HTTP requests");
	}
	//对于请求类型转换
	HttpServletRequest httpRequest = (HttpServletRequest) request;
	HttpServletResponse httpResponse = (HttpServletResponse) response;

	//获取准备过滤的参数名称
	String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
	boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
	//如果过滤的参数为空或者跳过Dispatch或者是不做任何的Filter,那么就从筛选器链中找其他的筛选器
	if (hasAlreadyFilteredAttribute || skipDispatch(httpRequest) || shouldNotFilter(httpRequest)) {

		// Proceed without invoking this filter...
		//筛选链
		filterChain.doFilter(request, response);
	}
	//否则执行这个filter
	else {
		// Do invoke this filter...
		//设置标识
		request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
		//执行内过滤器
		try {
			//执行内过滤器
			doFilterInternal(httpRequest, httpResponse, filterChain);
		}
		finally {
			// Remove the "already filtered" request attribute for this request.
			//移除标识
			request.removeAttribute(alreadyFilteredAttributeName);
		}
	}
}

  会看到**doFilterInternal()**方法是一个抽象方法,也就是说,继承这个类的子类需要重写这个方法才能完全的实现它的内容。否则功能将不会被实现。

	/**
	 * Same contract as for {@code doFilter}, but guaranteed to be
	 * just invoked once per request within a single request thread.
	 * See {@link #shouldNotFilterAsyncDispatch()} for details.
	 * <p>Provides HttpServletRequest and HttpServletResponse arguments instead of the
	 * default ServletRequest and ServletResponse ones.
	 */
	protected abstract void doFilterInternal(
			HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException;

如何实现一个自定义的Filter

第一种方式

第一步 实现Filter接口来实现

注意 要是它作为Spring的组件被Spring容器接管,要在类上加上@Component注解,或者使用@Bean注解进行注册

@Component
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println(" myfilter init");
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("myfilter execute");
    }
    @Override
    public void destroy() {
        System.out.println("myfilter destroy");
    }
}

使用@Bean注入

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean filterRegistration(){
        // 新建过滤器注册类
        FilterRegistrationBean registration = new FilterRegistrationBean();
        // 添加自定义 过滤器
        registration.setFilter(globalFilter());
        // 设置过滤器的URL模式
        registration.addUrlPatterns("/*");
        //设置过滤器顺序
        registration.setOrder(1);
        return registration;
    }
    @Bean
    public GlobalFilter globalFilter(){
        return new GlobalFilter();
    }
}


第二种方式

第一步 实现Filter接口

@Order(1)
@WebFilter(filterName = "MSecurity",urlPatterns = {"*.html"})
public class MSecurityFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response= (HttpServletResponse) servletResponse;
        System.out.println(request.getRequestURI());
        //检查是否是登录页面
        if(request.getRequestURI().equals("/web/index.html"))
            filterChain.doFilter(servletRequest,servletResponse);

        //检测用户是否登录
        HttpSession session =request.getSession();
        String status= (String) session.getAttribute("isLogin");
        if(status==null || !status.equals("true"))
        {
            try{  response.sendRedirect("/web/index.html");}catch (Exception e){}
        }
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {

    }
}

  上面内容是检查是否登陆如果登陆了就显示index页面如果不是则进入登陆页面

第二步 添加@ServletComponentScan注解

@SpringBootApplication
@ServletComponentScan(basePackages = "com.nihui.security")
public class MsSupplyAndSaleApplication {
    public static void main(String[] args) {
        SpringApplication.run(MsSupplyAndSaleApplication.class, args);
    }

}

内嵌过滤器的使用

  这里以一个项目实战的登陆功能最为演示,整合了结合了SpringSecurity的相关知识

第一步 实现WebSecurityConfigurerAdapter的扩展

public class WebSecurityConfigurerAdapterExt extends WebSecurityConfigurerAdapter {
	
	@Autowired
	private AuthenticationProvider authenticationProvider;
	@Autowired
	private AuthenticationSuccessHandler authenticationSuccessHandler;
	@Autowired
	private AuthenticationFailureHandler authenticationFailureHandler;
	@Autowired
	private AccessDeniedHandler accessDeniedHandler;
	@Autowired
	private AuthenticationEntryPoint authenticationEntryPoint;
	@Autowired
	private LogoutSuccessHandler logoutSuccessHandler;
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.authenticationProvider(authenticationProvider);
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		ValidateCodeFilter validateCodeFilter=new ValidateCodeFilter();
		http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
		
//		http.csrf().disable();

		http.authorizeRequests()
			//Spring Security 5.0 之后需要过滤静态资源
			.antMatchers("/mgmt/**").permitAll()
			.antMatchers("/swagger*/**","/webjars/**","/api/swagger.json").permitAll()
			.antMatchers("/login","/css/**","/js/**","/img.*").permitAll()
			.anyRequest().authenticated()
			.and()
			.formLogin()
			.usernameParameter("loginName").passwordParameter("password")
			.successHandler(authenticationSuccessHandler)
			.failureHandler(authenticationFailureHandler)
			.and()
			.logout().logoutSuccessHandler(logoutSuccessHandler).permitAll()
			.and()
			.exceptionHandling()
			.accessDeniedHandler(accessDeniedHandler)
			.authenticationEntryPoint(authenticationEntryPoint);
	}
	
	
	
}

第二步 向SpringBoot中注入扩展配置

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapterExt {

	@Override
	public void configure(WebSecurity web) throws Exception {
		// TODO Auto-generated method stub
		super.configure(web);
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.csrf().disable();
		http.headers().frameOptions().sameOrigin();
//		super.configure(http);

		//http 请求认证操作
		http.authorizeRequests()
		//Spring Security 5.0 之后需要过滤静态资源
		.antMatchers("/login").permitAll()
		.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
		.anyRequest().authenticated()
//        .anyRequest().permitAll()
		.and()
		.addFilterAfter(new DeptSelectFilter(), SessionManagementFilter.class)
		.formLogin()
		.usernameParameter("loginName").passwordParameter("password")
		.successHandler(authenticationSuccessHandler)
		.failureHandler(authenticationFailureHandler)
		.authenticationDetailsSource(authenticationDetailsSource)
		.and()
		.logout().logoutSuccessHandler(logoutSuccessHandler).permitAll()
		.and()
		.exceptionHandling()
		.accessDeniedHandler(accessDeniedHandler)
		.authenticationEntryPoint(authenticationEntryPoint);
		
		
	}
	//用户服务扩展
	@Bean
	public UserDetailsServiceExt userDetailsServiceExt() {
		return new UserConsumerAuthServiceImpl();
	}

}

第三步 设置一个登陆后置拦截器 DeptSelectFilter

  这个拦截器被设置到了登陆验证完成之后,用户用来选择对应的部门,如果部门认证通过的话就进入到index页面如果没有经过任何的处理,也就是说第一次登陆就会引导用户选择则对应的部门,并且回传部门信息。

public class DeptSelectFilter extends OncePerRequestFilter {

//	@Autowired
//	private UserInfoFeignClient userInfoFeignClient;

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		boolean validDept = false;
		//获取请求的URI字符串
		String requesturi = request.getRequestURI();
		//userInfoFeignClient.getUserInfo()
		//判断是否登录
		if(SecurityContextHolderExt.isLogin()) {
			if("/dept".equals(requesturi)) {
				validDept = false;
				//匹配字符串内容为     /v[数字]/dept.*
			}else if(requesturi.matches("/v[0-9]+/dept.*")) {
				validDept = false;
			}else {
				validDept = true;
			}
		}
		
		
		
		if(validDept){
			List<DeptExt> deptExts = SecurityContextHolderExt.getDepts();
			if(deptExts==null || deptExts.size()==0) {
				if(AjaxUtil.isAjaxRequest(request)) {
					ResultResp<UserResp> resultResp=new ResultResp<>();
					ExceptionMsg exceptionMsg=new ExceptionMsg();
					exceptionMsg.setErrorMsg("请先选择部门");
					resultResp.setExceptionMsg(exceptionMsg);
					resultResp.setStatus(ResultRespStatus.EXCEPTION);
					
					ResponseUtil.doResponse(response, HttpServletResponse.SC_UNAUTHORIZED, MediaType.APPLICATION_JSON_VALUE, resultResp.toString());
			        return;  
				}else {
					response.sendRedirect("/dept");
					return;
				}
			}
		}


		filterChain.doFilter(request, response);
	}

}

  上面方法就实现了对OncePerRequestFilter拦截器的doFilterInternal()方法的扩展,并且最后结束的时候将请求引入到了Filter链路中。filterChain.doFilter(request, response)。

OncePerRequestFilter 类继承关系扩展

在这里插入图片描述
  通过上图的类关系图可以看到,在SpringMVC中对于Filter的扩展都是继承了OncePerRequestFilter。其中都是实现了doFilterInternal()的方法扩展。

总结

  上面内容主要讲述了在实际的开发中如何使用OncePerRequestFilter过滤器。并且结合了一个小例子,描述了在实际开发中如何使用Filter,当然在实际开发中使用到Filter的场景还有其他的使用场景。这里只是这个应用场景的冰山一角。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nihui123

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值