近期梳理代码时,我偶然发现自定义的 Jwt Filter 竟然执行了两次 doFilter,这让我心里一惊!虽然目前未发现对项目的直接影响,但这就像一颗定时炸弹,随时可能引爆。我不敢掉以轻心,加班彻查了一番,最终弄清了原因,开心下班!
问题描述
事情是这样的:我在用 XCodeMap 走读代码时,在序列图上发现 SimpleJwtAuthTokenFilter 这个类调用了两次 doFilter。起初我以为是 XCodeMap 出错了,于是用 Idea Debug 验证了一下。结果发现断点确实触发了两次,这下我不敢掉以轻心了——显然是真的出问题了。
继续用 XCodeMap 观察上下文数据,查看 doFilter 的用法,发现:
- 第一次是在 org.springframework.security.web.FilterChainProxy$VirtualFilterChain 处调用
- 第二次是在 org.apache.catalina.core.ApplicationFilterChain 处调用
简单说明一下,FilterChainProxy$VirtualFilterChain 是 Spring Security 的核心组件,它实现了 FilterChain 接口,但同时也作为一个普通的 Filter 注册到了 Tomcat 容器。而 ApplicationFilterChain 则是 Tomcat 容器的标准 FilterChain 实现。
进一步查看发现,SimpleJwtAuthTokenFilter 不仅插入到了 VirtualFilterChain 中(作为 Spring Security Chain 的一部分),还插入到了 ApplicationFilterChain 中(作为 Tomcat 容器的一部分)。
这显然不符合预期。
然而,更令人疑惑的是,为什么这种情况没有对项目造成实质性的影响呢?
为什么项目没受影响
用 XCodeMap 查看 SimpleJwtAuthTokenFilter doFilter 的执行情况,可以发现两次走过的代码不一样。
第一次执行,走过的代码(注意看蓝色高亮部分)
第二次执行,走过的代码(注意看蓝色高亮部分)
XCodeMap 会把程序走过的代码高亮出来,因此可以一眼看出某个函数的分支走向。查看上面的代码会发现,SimpleJwtAuthTokenFilter 继承了 OncePerRequestFilter,它有一个防重复执行的设计。在第一次执行时,会在 request 的 attribute 中设置标记,后续如果还触发了这个Filter,就会检查到这个标记 hasAlreadyFilteredAttribute,然后直接跳过。
还好,虚惊一场!确认对项目没有直接影响后,我松了一口气。
我把这个发现告诉了组长,并表示暂时可以不修复。组长赞赏了我的行为,但同时指出了一个重要观点:虽然目前没有影响,但不能保证将来不会出问题。他解释说,并非每个 Filter 都会继承 OncePerRequestFilter,因此最好还是查清楚核心原因——为什么会注册到两个不同的地方去。这样才能彻底排除隐患。
为什会注册到两个地方
继续用 XCodeMap 的一个对象追踪功能,可以发现两个关键代码。
- SimpleJwtAuthTokenFilter 在doCreateBean 阶段的时候,被插入到了 ApplicationFilterChain
@Bean
public SimpleJwtAuthTokenFilter jwtAuthenticationTokenFilter(){
return new SimpleJwtAuthTokenFilter();
}
- 在 HttpSecurity.addFilterBefore 的时候被插入了 FilterChainProxy$VirtualFilterChain
@Autowired
private SimpleJwtAuthTokenFilter simpleJwtAuthTokenFilter;
// 自定义权限拦截器JWT过滤器
.and()
.addFilterBefore(simpleJwtAuthTokenFilter, UsernamePasswordAuthenticationFilter.class);
这是一个典型的设计冲突。当一个 Filter 被定义为 Bean 并由 SpringBoot 管理时,它会自动注册到 Tomcat 的 FilterChain 中。然而,Spring Security 同时也会将其插入到自己的虚拟 FilterChain 中。这就导致了 Filter 的重复执行。
解决办法
一个简单的解决方法是将 Spring Security 的 Filter 从 Spring Bean 生命周期中移除,直接通过 new 关键字创建。然而,在实际项目中,如果该 Filter 有多个依赖项,这种方法可能难以实施,甚至可能导致依赖关系混乱。
考虑到这些因素,继承 OncePerRequestFilter 似乎是一个更可靠的方案。Spring Security 提供这个抽象类,很可能就是为了解决这类问题。
所以,写代码的同学就要注意了,Spring Security 的 Filter 一定要记得继承一下 OncePerRequestFilter !另外提一嘴,XCodeMap 是一个走读代码的插件,非常好用!