zoukankan      html  css  js  c++  java
  • 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,启动的时候注册好方便调用。。。

  • 相关阅读:
    自增自减
    字符串处理函数
    指针总结指向const的指针、const指针、指向const指针的const指针
    Jenkins基础篇 系列之-—04 节点配置
    soapUI系列之—-07 调用JIRA Rest API接口【例】
    测试人员转正考核方向
    linux系列之-—03 常见问题
    Jenkins基础篇 系列之-—03 配置用户和权限控制
    linux系列之-—01 shell编程笔记
    Jenkins基础篇 系列之-—02 修改Jenkins用户的密码
  • 原文地址:https://www.cnblogs.com/54hsh/p/12773869.html
Copyright © 2011-2022 走看看