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