Spring Security自定义过滤器多次执行的问题

博客探讨了Spring Security中自定义Filter执行两次的问题,原因是FilterChainProxy和ApplicationFilterChain两条过滤器链都包含该Filter。分析了FilterChainProxy的doFilter方法,发现当所有额外过滤器执行完后,会触发原始过滤器链ApplicationFilterChain。Filter的添加是通过Spring的Bean管理和Tomcat的生命周期完成的。解决办法在于理解并正确配置过滤器的注册方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

public class TokenFilter implements Filter {
    public static final String HEADER_AUTH_NAME = "Authorization";

    @Autowired
    JWTProvider jwtProvider;

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
        try {
            HttpServletRequest httpServletRequest = (HttpServletRequest) req;
            String authToken = httpServletRequest.getHeader(HEADER_AUTH_NAME);
            if (StringUtils.isNotBlank(authToken)) {
                // 从自定义tokenProvider中解析用户
                Authentication authentication = this.jwtProvider.getAuthentication(authToken);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
            // 调用后续的Filter,如果上面的代码逻辑未能复原“session”,SecurityContext中没有想过信息,后面的流程会检测出"需要登录"
            chain.doFilter(req, res);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
}

当我们直接或间接的继承了Filter之后,Spring Security 会自动将过滤器添加多执行列表中。
如果我们仍将其添加到 WebSecurityConfigurerAdapter 中

@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {


    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Autowired
    private LoginAuthenticationFilter loginAuthenticationFilter;

    @Autowired
    private TokenFilter tokenFilter;

    @Autowired
    private CorsConfigurationSource configurationSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(loginAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
//                .addFilterBefore(tokenFilter,UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                .authorizeRequests()
                .antMatchers("/login","/register").permitAll()
                .anyRequest().authenticated()
                .and()
                .cors().configurationSource(configurationSource)
                // 关闭 csrf 保护
                .and()
                .csrf().disable()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                        // 返回 json 格式的数据
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        PrintWriter writer = httpServletResponse.getWriter();
                        Map<String, Object> map = new HashMap<>(16);
                        map.put("status:", 200);
                        map.put("msg:","注销登录成功!");
                        writer.write(new ObjectMapper().writeValueAsString(map));
                        writer.flush();
                        writer.close();
                    }
                });
    }

    @Bean
    public PasswordEncoder passwordEncoderBean() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
}

就会对此过滤词执行两次。

分界线

在这里插入图片描述
有个朋友指出了问题,我会看了上面的说法,感觉之前的记录有比较大的问题。并没有把结论是怎么得来的放上去,现在回想也想不起来了。索性再次研究一下Spring中filter的运行机制。
首先从自定义的Filter执行了两次的问题出发,我们发现,这两次的执行是两条不同的过滤器链分别为:FilterChainProxy和ApplicationFilterChain。如下面两张图。
请添加图片描述
请添加图片描述
接下来我们就要了解一下这两条链
首先是FilterChainProxy:
下面这段代码是FilterChainProxy的doFilter方法也就是我们的TokenFilter的doFilter方法调用的chain.doFilter。

	@Override
		public void doFilter(ServletRequest request, ServletResponse response)
				throws IOException, ServletException {
			if (currentPosition == size) {
				if (logger.isDebugEnabled()) {
					logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
							+ " reached end of additional filter chain; proceeding with original chain");
				}

				// Deactivate path stripping as we exit the security filter chain
				this.firewalledRequest.reset();

				originalChain.doFilter(request, response);
			}
			else {
				currentPosition++;

				Filter nextFilter = additionalFilters.get(currentPosition - 1);

				if (logger.isDebugEnabled()) {
					logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
							+ " at position " + currentPosition + " of " + size
							+ " in additional filter chain; firing Filter: '"
							+ nextFilter.getClass().getSimpleName() + "'");
				}

				nextFilter.doFilter(request, response, this);
			}
		}
	}

从上面的代码我们可以看出当currentPosition == size时即chain内部的filter都执行之后,接下来会出现另外一个filterChain->
originalChain。而这个originalChain正是ApplicationFilterChain。自然的,接下来就会执行到ApplicationFilterChain中Filter,而这两个FilterChain中都包含了我们定义的TokenFilter。
在这里插入图片描述
了解到是因为两条FilterChain中都有customFilter的问题,但是为什么会是这样呢。
FilterChainProxy时我们通过addFilterBefore添加的
同时通过debug发现,
ApplicationFilterChain是StandardWrapperValue的invoke方法内创建的

ApplicationFilterChain filterChain =
                ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

这里wrapper的值是关键
在这里插入图片描述
StandardEngine[Tomcat].StandardHost[localhost].TomcatEmbeddedContext[].StandardWrapper[dispatcherServlet]
继续往下看来到ApplicationFilterFactory.createFilterChain()内部
在ApplicationFilterFactory的第83行有这样两行代码:

StandardContext context = (StandardContext) wrapper.getParent();
        FilterMap filterMaps[] = context.findFilterMaps();

在这里插入图片描述
通过这两行代码可知ApplicationChain的Filter来源是wrapper.parent.filterMaps的值。
在ApplicationFilterFactory的109-115行是遍历这个map,将值add到ApplicationFilterChain中。

现在问题来到了wrapper.parent.filterMaps是何时初始化的问题上。
StandardWrapperValue103行

StandardWrapper wrapper = (StandardWrapper) getContainer();

在这里插入图片描述
通过ApplicationContext.addFilter向TomcatEmbeddedContext中添加FilterMap。这里不是通过addFilterBefore。具体通过什么方式,感兴趣的可以再去深入探究一下。
大致需要满足
1.Filter必须是Spring的Bean对象,才会被添加。
2.必须是Filter。

Spring Security 中的所有过滤器及其作用如下1. `ChannelProcessingFilter`:根据请求的协议(http 或 https)决定是否需要使用 SSL 安全连接。 2. `SecurityContextPersistenceFilter`:在请求处理过程中,负责将安全上下文存储到 `SecurityContextHolder` 中,并在请求完成后恢复上下文。 3. `ConcurrentSessionFilter`:用于处理并发会话控制,检测并处理用户多次登录或会话超过限制的情况。 4. `LogoutFilter`:处理注销功能,清除当前用户的身份认证信息并执行一些清理操作。 5. `UsernamePasswordAuthenticationFilter`:处理基于表单的用户名密码身份验证,验证用户的用户名和密码。 6. `DefaultLoginPageGeneratingFilter`:生成默认的登录页面,如果应用程序没有自定义登录页面,则使用过滤器。 7. `DefaultLogoutPageGeneratingFilter`:生成默认的注销页面,如果应用程序没有自定义注销页面,则使用过滤器。 8. `BasicAuthenticationFilter`:处理 HTTP Basic 认证,从请求头中提取用户名和密码进行验证。 9. `RequestCacheAwareFilter`:处理请求的缓存,当用户需要进行身份验证时,将当前请求缓存起来以便后续处理。 10. `SecurityContextHolderAwareRequestFilter`:对请求进行包装,以便在请求处理过程中可以访问 `SecurityContextHolder` 中的安全上下文。 11. `RememberMeAuthenticationFilter`:处理“记住我”功能,通过 cookie 或 token 记住用户的身份信息。 12. `AnonymousAuthenticationFilter`:处理匿名用户的身份验证,为未经身份验证的用户分配一个临时身份。 13. `SessionManagementFilter`:管理用户会话,处理会话并发控制、会话超时和会话固定攻击等问题。 14. `ExceptionTranslationFilter`:处理身份验证和授权过程中的异常,例如未经授权的访问或身份验证失败。 15. `FilterSecurityInterceptor`:实施访问控制决策,根据配置的访问规则拦截请求并进行授权判断。 这些过滤器共同工作,通过链式调用来保护应用程序的资源,并提供身份验证和授权功能。需要注意的是,具体使用哪些过滤器取决于你的配置和需求,在实际使用中可以选择性地配置和扩展这些过滤器
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值