車禍現場:整合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,啟動的時候注冊好方便調用。。。