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,启动的时候注册好方便调用。。。
千万不要试图去研究 研究了很久都整不明白的东西,或许是层次不到,境界未到,也或许是从未在实际的应用场景接触过,这种情况下去研究,只会事倍功半,徒劳一番罢了。能做的就是不断的沉淀知识,保持一颗积极向上的学习心态,相信终有一天所有的困难都会迎刃而解。