spring security自定義filter重復執行問題


車禍現場:整合spring security的時候,自定義一個filter,啟動后發現一次請求filter會重復執行了兩遍,最終查閱資料得到解決,記錄一下。

security的config配置如下:

/**
 * 軟件版權:流沙~~
 * 修改日期   修改人員     修改說明
 * =========  ===========  =====================
 * 2019/11/26    liusha   新增
 * =========  ===========  =====================
 */
package com.sand.security.web.config;

import com.sand.security.web.filter.MyAuthenticationTokenGenericFilter;
import com.sand.security.web.handler.MyAccessDeniedHandler;
import com.sand.security.web.provider.MyAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * 功能說明:自定義Spring Security配置
 * 開發人員:@author liusha
 * 開發日期:2019/11/26 10:34
 * 功能描述:安全認證基礎配置,開啟 Spring Security
 * 方法級安全注解 @EnableGlobalMethodSecurity
 * prePostEnabled:決定Spring Security的前注解是否可用 [@PreAuthorize,@PostAuthorize,..]
 * secureEnabled:決定是否Spring Security的保障注解 [@Secured] 是否可用
 * jsr250Enabled:決定 JSR-250 annotations 注解[@RolesAllowed..] 是否可用.
 */
@Configurable
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  /**
   * 用戶信息服務
   */
  @Autowired
  private UserDetailsService userDetailsService;

  /**
   * 認證管理器:使用spring自帶的驗證密碼的流程
   * <p>
   * 負責驗證、認證成功后,AuthenticationManager 返回一個填充了用戶認證信息(包括權限信息、身份信息、詳細信息等,但密碼通常會被移除)的 Authentication 實例。
   * 然后再將 Authentication 設置到 SecurityContextHolder 容器中。
   * AuthenticationManager 接口是認證相關的核心接口,也是發起認證的入口。
   * 但它一般不直接認證,其常用實現類 ProviderManager 內部會維護一個 List<AuthenticationProvider> 列表,
   * 存放里多種認證方式,默認情況下,只需要通過一個 AuthenticationProvider 的認證,就可被認為是登錄成功
   *
   * @return
   * @throws Exception
   */
  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }

  /**
   * 密碼驗證方式
   * 默認加密方式為BCryptPasswordEncoder
   *
   * @return
   */
  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(6);
  }

  /**
   * 加載自定義的驗證失敗處理方式
   *
   * @return
   */
  @Bean
  public MyAccessDeniedHandler myAccessDeniedHandler() {
    return new MyAccessDeniedHandler();
  }

  /**
   * 加載自定義的token校驗過濾器
   *
   * @return
   */
  @Bean
  public MyAuthenticationTokenGenericFilter myAuthenticationTokenGenericFilter() {
    return new MyAuthenticationTokenGenericFilter();
  }

  /**
   * 靜態資源
   * 不攔截靜態資源,所有用戶均可訪問的資源
   */
  @Override
  public void configure(WebSecurity webSecurity) {
    webSecurity.ignoring().antMatchers("/", "/css/**", "/js/**", "/images/**");
  }

  /**
   * 密碼驗證方式
   * 將用戶信息和密碼加密方式進行注入
   *
   * @param auth
   * @throws Exception
   */
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService)
        .passwordEncoder(passwordEncoder());
    // 關閉密碼驗證方式
//        .passwordEncoder(NoOpPasswordEncoder.getInstance());
  }

  @Override
  protected void configure(HttpSecurity httpSecurity) throws Exception {
    MyAuthenticationProvider authenticationProvider = new MyAuthenticationProvider();
    authenticationProvider.setUserDetailsService(userDetailsService);
    httpSecurity
        // 關閉crsf攻擊,允許跨越訪問
        .csrf().disable()
        // 自定義登錄認證方式
        .authenticationProvider(authenticationProvider)
        // 自定義驗證處理器
        .exceptionHandling().accessDeniedHandler(myAccessDeniedHandler()).and()
        // 不創建HttpSession,不使用HttpSession來獲取SecurityContext
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
        .authorizeRequests()
        // 允許登錄接口post訪問
        .antMatchers(HttpMethod.POST, "/auth/login").permitAll()
        // 允許驗證碼接口post訪問
        .antMatchers(HttpMethod.POST, "/valid/code/*").permitAll().and();
//        // 任何尚未匹配的URL只需要驗證用戶即可訪問
//        .anyRequest().authenticated()
    httpSecurity.addFilterBefore(myAuthenticationTokenGenericFilter(), UsernamePasswordAuthenticationFilter.class);
  }
}

自定義的filter配置如下:

/**
 * 軟件版權:流沙~~
 * 修改日期   修改人員     修改說明
 * =========  ===========  =====================
 * 2020/4/19    liusha   新增
 * =========  ===========  =====================
 */
package com.sand.security.web.filter;

import com.sand.common.util.lang3.StringUtil;
import com.sand.security.web.IUserAuthenticationService;
import com.sand.security.web.handler.MyAuthExceptionHandler;
import com.sand.security.web.util.AbstractTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.util.CollectionUtils;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.List;
import java.util.Objects;

/**
 * 功能說明:token過濾器
 * 開發人員:@author liusha
 * 開發日期:2020/4/19 17:30
 * 功能描述:用戶合法性校驗
 */
@Slf4j
public class MyAuthenticationTokenGenericFilter extends GenericFilterBean {
  /**
   * MyAuthenticationTokenGenericFilter標記
   */
  private static final String FILTER_APPLIED = "__spring_security_myAuthenticationTokenGenericFilter_filterApplied";
  /**
   * TODO 過濾元數據,后續自己實現
   */
  private FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
  /**
   * 用戶基礎服務接口
   */
  @Autowired
  private IUserAuthenticationService userAuthenticationService;

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    HttpServletResponse httpResponse = (HttpServletResponse) response;
    // 確保每個請求僅應用一次過濾器:spring容器托管的GenericFilterBean的bean,都會自動加入到servlet的filter chain,
    // 而WebSecurityConfig中myAuthenticationTokenGenericFilter定義的bean還額外把filter加入到了spring security中,所以會出現執行兩次的情況。
//    if (httpRequest.getAttribute(FILTER_APPLIED) != null) {
//      chain.doFilter(httpRequest, httpResponse);
//      return;
//    }
//    httpRequest.setAttribute(FILTER_APPLIED, Boolean.TRUE);
    log.info("~~~~~~~~~用戶合法性校驗~~~~~~~~~");
    // 白名單直接驗證通過
    if (isPermitUrl(httpRequest, httpResponse, chain)) {
      chain.doFilter(httpRequest, httpResponse);
      return;
    }
    try {
      // 非白名單需驗證其合法性(非白名單請求必須帶token)
      String authHeader = httpRequest.getHeader(AbstractTokenUtil.TOKEN_HEADER);
      final String authToken = StringUtil.substring(authHeader, 7);
      userAuthenticationService.checkAuthToken(authToken);
      chain.doFilter(httpRequest, httpResponse);
    } catch (Exception e) {
      log.error("MyAuthenticationTokenGenericFilter異常", e);
      MyAuthExceptionHandler.accessDeniedException(e, httpResponse);
    }
  }

  /**
   * 是否是白名單
   *
   * @param request  request
   * @param response response
   * @param chain    chain
   * @return true-是白名單 false-不是白名單
   */
  public boolean isPermitUrl(ServletRequest request, ServletResponse response, FilterChain chain) {
    if (Objects.isNull(filterInvocationSecurityMetadataSource)) {
      try {
        // 獲取security配置的白名單信息
        Class clazz = chain.getClass();
        Field field = clazz.getDeclaredField("additionalFilters");
        field.setAccessible(true);
        List<Filter> filters = (List<Filter>) field.get(chain);
        for (Filter filter : filters) {
          if (filter instanceof FilterSecurityInterceptor) {
            filterInvocationSecurityMetadataSource = ((FilterSecurityInterceptor) filter).getSecurityMetadataSource();
          }
        }
      } catch (Exception e) {
        log.error("security過濾元數據獲取異常,白名單驗證失敗", e);
        return false;
      }
    }
    FilterInvocation filterInvocation = new FilterInvocation(request, response, chain);
    Collection<ConfigAttribute> permitUrls = filterInvocationSecurityMetadataSource.getAttributes(filterInvocation);
    boolean isPermitUrl = false;
    if (!CollectionUtils.isEmpty(permitUrls)) {
      isPermitUrl = permitUrls.toString().contains("permitAll");
    }
    if (isPermitUrl) {
      log.info("白名單請求url:{}", ((HttpServletRequest) request).getRequestURI());
    } else {
      log.info("非白名單請求url:{}", ((HttpServletRequest) request).getRequestURI());
    }
    return isPermitUrl;
  }
}

分析原因:MyAuthenticationTokenGenericFilter是繼承自GenericFilterBean,由spring容器托管,會自動加入到servlet的filter chain中,而spring security的config配置中又把filter注冊到了spring security的容器中,因此在調用UsernamePasswordAuthenticationFilter鑒權之前和鑒權之后先后會各執行一次。

  @Bean
  public MyAuthenticationTokenGenericFilter myAuthenticationTokenGenericFilter() {
    return new MyAuthenticationTokenGenericFilter();
  }

解決方案:

1)、security的config配置更改如下代碼

//  @Bean
//  public MyAuthenticationTokenGenericFilter myAuthenticationTokenGenericFilter() {
//    return new MyAuthenticationTokenGenericFilter();
//  }


httpSecurity.addFilterBefore(new MyAuthenticationTokenGenericFilter(), UsernamePasswordAuthenticationFilter.class);

2)、或者更改自定義的filter配置代碼,將以下代碼注釋打開

    if (httpRequest.getAttribute(FILTER_APPLIED) != null) {
      chain.doFilter(httpRequest, httpResponse);
      return;
    }
  httpRequest.setAttribute(FILTER_APPLIED, Boolean.TRUE);

推薦使用第2種,因為在實際開發過程中可能會需要用到MyAuthenticationTokenGenericFilter,啟動的時候注冊好方便調用。。。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM