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,启动的时候注册好方便调用。。。

 

posted @ 2020-05-11 12:54  聚散彡流沙  阅读(2800)  评论(0编辑  收藏  举报