Spring Security中捕获RequestRejectedException后仍记录未捕获错误日志的问题求助
问题原因分析
这个情况的核心原因是过滤器执行顺序的问题:你的RequestRejectedExceptionHandler过滤器虽然设置了@Order(Ordered.HIGHEST_PRECEDENCE),但实际上它的执行顺序在Spring Cloud Sleuth的ExceptionLoggingFilter之后。
从你提供的异常栈可以看到,ExceptionLoggingFilter是在Spring Security过滤器链的上游执行的。当StrictHttpFirewall抛出RequestRejectedException时,异常会沿着过滤器链反向传播:先被ExceptionLoggingFilter捕获,它会记录一条ERROR级别的“未捕获异常”日志,然后将异常重新抛出;之后异常才会传到你的自定义过滤器,此时你虽然处理了异常并返回400响应,但ERROR日志已经被记录下来了。
另外,ExceptionLoggingFilter的源码逻辑就是捕获所有通过它的异常,打日志后重新抛出,所以只要它在你的过滤器之前,就一定会先记录这条日志。
解决方案
有两种靠谱的解决方式,推荐第一种,因为它更贴合Spring Security的设计:
方案1:使用Spring Security内置的RequestRejectedHandler处理异常
Spring Security本身就提供了处理RequestRejectedException的扩展点,我们可以自定义一个RequestRejectedHandler,让异常在Spring Security内部就被处理,不会传播到Servlet Filter链中,自然也就不会触发ExceptionLoggingFilter的日志。
步骤如下:
- 自定义
RequestRejectedHandler实现类:
@Component public class CustomRequestRejectedHandler implements RequestRejectedHandler { private static final Logger log = LoggerFactory.getLogger(CustomRequestRejectedHandler.class); @Override public void handle(HttpServletRequest request, HttpServletResponse response, RequestRejectedException ex) throws IOException { // 这里记录WARN级别日志即可,不需要ERROR log.warn("请求被拒绝:非法URL格式,URL={}", request.getRequestURL(), ex); // 设置400响应 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); // 可以写入自定义的JSON响应体 response.getWriter().write("{\"code\":400,\"message\":\"非法的请求URL格式\"}"); } }
- 在Spring Security配置类中配置这个处理器:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final CustomRequestRejectedHandler customRequestRejectedHandler; public SecurityConfig(CustomRequestRejectedHandler customRequestRejectedHandler) { this.customRequestRejectedHandler = customRequestRejectedHandler; } @Override protected void configure(HttpSecurity http) throws Exception { http // 这里添加你的其他Security配置 .authorizeRequests() .anyRequest().authenticated() .and() .exceptionHandling() // 配置自定义的RequestRejectedHandler .requestRejectedHandler(customRequestRejectedHandler); } // 如果需要自定义StrictHttpFirewall的规则(比如是否允许双斜杠),可以添加这个Bean @Bean public StrictHttpFirewall strictHttpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); // 如果你需要允许URL中的双斜杠,可以打开下面的注释,但不建议,存在安全风险 // firewall.setAllowUrlEncodedDoubleSlash(true); return firewall; } @Override public void configure(WebSecurity web) throws Exception { web.httpFirewall(strictHttpFirewall()); } }
这样配置后,当StrictHttpFirewall抛出异常时,Spring Security会直接调用我们自定义的处理器,异常不会传播到Servlet Filter链,ExceptionLoggingFilter也就不会记录那条ERROR日志了。
方案2:调整自定义过滤器的执行顺序,确保它在ExceptionLoggingFilter之前
如果你坚持使用Servlet Filter的方式处理,需要确保你的过滤器在ExceptionLoggingFilter之前执行。
Spring Cloud Sleuth的ExceptionLoggingFilter默认的Order是Ordered.HIGHEST_PRECEDENCE + 9,所以我们可以通过FilterRegistrationBean手动注册过滤器,设置一个更小的Order值(优先级更高):
- 移除原过滤器类上的
@Component注解,然后创建一个配置类注册过滤器:
@Configuration public class FilterRegistrationConfig { @Bean public FilterRegistrationBean<RequestRejectedExceptionHandler> requestRejectedHandlerFilter() { FilterRegistrationBean<RequestRejectedExceptionHandler> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new RequestRejectedExceptionHandler()); // 设置比ExceptionLoggingFilter更高的优先级 registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); // 匹配所有请求路径 registrationBean.addUrlPatterns("/*"); return registrationBean; } }
- 确保原过滤器的
doFilterInternal方法中处理完响应后,不要重新抛出异常(你的代码已经做到了这一点):
@Slf4j public class RequestRejectedExceptionHandler extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { filterChain.doFilter(request, response); } catch (RequestRejectedException e) { log.warn("400 - Bad Request: 非法URL格式", e); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write("{\"code\":400,\"message\":\"非法的请求URL格式\"}"); // 不要抛出异常,避免继续传播 } } }
这样,你的过滤器会先于ExceptionLoggingFilter执行,当异常抛出时会被你的过滤器捕获并处理,不会传播到ExceptionLoggingFilter,也就不会产生那条多余的ERROR日志了。
内容的提问来源于stack exchange,提问作者amasuKAKAROT

