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的場景還有其他的使用場景。這裏只是這個應用場景的冰山一角。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章