在 Spring Boot 3.3.3 中,我尝试使用默认的 HandlerExceptionResolver 来捕获安全过滤器链中发生的任何异常,以便在控制器使用的相同 exceptHandler ControllerAdvise 中处理它们。 我可以看到异常一直到 ExceptionHandler 的内部,但返回的 modelAndView 没有发生任何事情。 浏览器显示空白页面。
为了测试这一点,我有一个示例过滤器,其中遇到特定 URI 时抛出异常:
@Slf4j
public class SampleFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.debug( "entered SampleFilter" );
if ( request.getRequestURI().endsWith("does-not-exist") ) {
log.debug( "throwing exception" );
throw new SampleException("does-not-exist does not exist! Big surprise!");
}
filterChain.doFilter(request, response);
}
}
另一个过滤器捕获所有异常并将它们发送到解析器
@Slf4j
@Component
public class FilterChainExceptionHandlerFilter extends OncePerRequestFilter {
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (Exception e) {
log.error("Spring Security Filter Chain Exception:", e);
resolver.resolveException(request, response, null, e);
}
}
}
我将这些注册到 securityFilterChain 中
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
@Autowired
private FilterChainExceptionHandlerFilter filterChainExceptionHandlerFilter;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.anonymous(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.addFilterBefore( filterChainExceptionHandlerFilter, DisableEncodeUrlFilter.class )
.addFilterBefore( new SampleFilter(), SecurityContextHolderFilter.class )
.build();
}
}
我在ControllerAdvice中定义了一个专门针对抛出异常的ExceptionHandler
@Slf4j
@ControllerAdvice
public class ExceptionController {
@ExceptionHandler( {SampleException.class} )
public ModelAndView handleSampleException(HttpServletRequest req, Exception e) {
log.debug( "SampleException: setup error page for " + e.getLocalizedMessage() );
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error");
modelAndView.addObject( "message", e.getLocalizedMessage() );
return modelAndView;
}
}
然后它应该被错误控制器拾取
@Slf4j
@Controller
public class SampleErrorController implements ErrorController {
@RequestMapping( "/error" )
public String showError( HttpServletRequest request,
Model model ) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (status != null) {
Integer statusCode = Integer.valueOf(status.toString());
model.addAttribute( "statusCode", statusCode );
}
return "error";
}
}
并发送到我拥有的简单 error.html Thymeleaf 模板。
输入不存在且不会从 SampleFilter 引发异常的 URL 会很好地发送到模板。 我看到我的异常已记录在 ExceptionHandler 方法中,但 ModelAndView 似乎从未传递到 Thymeleaf。
日志
:: Spring Boot :: (v3.3.3)
2024-09-22 15:30:51,778 INFO [restartedMain] o.s.b.StartupInfoLogger: Starting SampleApplication using Java 17.0.12 with PID 3084906 (/home/user/github/spring-servlet-filter-exception/target/classes started by user in /home/user/github/spring-servlet-filter-exception)
2024-09-22 15:30:51,779 DEBUG [restartedMain] o.s.b.StartupInfoLogger: Running with Spring Boot v3.3.3, Spring v6.1.12
2024-09-22 15:30:51,780 INFO [restartedMain] o.s.b.SpringApplication: No active profile set, falling back to 1 default profile: "default"
2024-09-22 15:30:52,503 DEBUG [restartedMain] o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver: ControllerAdvice beans: 1 @ExceptionHandler, 1 ResponseBodyAdvice
2024-09-22 15:30:52,543 DEBUG [restartedMain] o.s.w.f.GenericFilterBean: Filter 'filterChainExceptionHandlerFilter' configured for use
2024-09-22 15:30:52,575 WARN [restartedMain] o.s.b.a.s.s.UserDetailsServiceAutoConfiguration:
Using generated security password: d4c20939-34c6-4bc1-ab92-c176e0cba968
This generated password is for development use only. Your security configuration must be updated before running your application in production.
2024-09-22 15:30:52,598 INFO [restartedMain] o.s.s.c.a.a.c.InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer: Global AuthenticationManager configured with UserDetailsService bean with name inMemoryUserDetailsManager
2024-09-22 15:30:52,673 DEBUG [restartedMain] o.s.w.s.h.AbstractHandlerMethodMapping: 2 mappings in 'requestMappingHandlerMapping'
2024-09-22 15:30:52,689 DEBUG [restartedMain] o.s.w.s.h.SimpleUrlHandlerMapping: Patterns [/webjars/**, /**] in 'resourceHandlerMapping'
2024-09-22 15:30:52,708 DEBUG [restartedMain] o.s.s.w.DefaultSecurityFilterChain: Will secure any request with filters: FilterChainExceptionHandlerFilter, DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SampleFilter, SecurityContextHolderFilter, HeaderWriterFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, ExceptionTranslationFilter
2024-09-22 15:30:52,727 WARN [restartedMain] o.s.s.c.a.w.b.WebSecurity:
********************************************************************
********** Security debugging is enabled. *************
********** This may include sensitive information. *************
********** Do not use in a production system! *************
********************************************************************
2024-09-22 15:30:52,764 DEBUG [restartedMain] o.s.w.s.m.m.a.RequestMappingHandlerAdapter: ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice
2024-09-22 15:30:52,852 INFO [restartedMain] o.s.b.StartupInfoLogger: Started SampleApplication in 1.326 seconds (process running for 1.697)
2024-09-22 15:31:39,910 INFO [http-nio-8080-exec-1] o.s.w.s.FrameworkServlet: Initializing Servlet 'dispatcherServlet'
2024-09-22 15:31:39,910 DEBUG [http-nio-8080-exec-1] o.s.w.s.DispatcherServlet: Detected StandardServletMultipartResolver
2024-09-22 15:31:39,911 DEBUG [http-nio-8080-exec-1] o.s.w.s.DispatcherServlet: Detected AcceptHeaderLocaleResolver
2024-09-22 15:31:39,911 DEBUG [http-nio-8080-exec-1] o.s.w.s.DispatcherServlet: Detected FixedThemeResolver
2024-09-22 15:31:39,912 DEBUG [http-nio-8080-exec-1] o.s.w.s.DispatcherServlet: Detected org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator@35c882e0
2024-09-22 15:31:39,912 DEBUG [http-nio-8080-exec-1] o.s.w.s.DispatcherServlet: Detected org.springframework.web.servlet.support.SessionFlashMapManager@5e3d86ed
2024-09-22 15:31:39,912 DEBUG [http-nio-8080-exec-1] o.s.w.s.FrameworkServlet: enableLoggingRequestDetails='true': request parameters and headers will be shown which may lead to unsafe logging of potentially sensitive data
2024-09-22 15:31:39,912 INFO [http-nio-8080-exec-1] o.s.w.s.FrameworkServlet: Completed initialization in 2 ms
2024-09-22 15:31:39,940 DEBUG [http-nio-8080-exec-1] o.s.s.w.FilterChainProxy: Securing GET /does-not-exist
2024-09-22 15:31:39,943 DEBUG [http-nio-8080-exec-1] o.e.s.SampleFilter: entered SampleFilter
2024-09-22 15:31:39,944 DEBUG [http-nio-8080-exec-1] o.e.s.SampleFilter: throwing exception
2024-09-22 15:31:39,944 ERROR [http-nio-8080-exec-1] o.e.s.FilterChainExceptionHandlerFilter: Spring Security Filter Chain Exception:
org.example.controller.SampleException: does-not-exist does not exist! Big surprise!
at org.example.security.SampleFilter.doFilterInternal(SampleFilter.java:21)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
at org.example.security.FilterChainExceptionHandlerFilter.doFilterInternal(FilterChainExceptionHandlerFilter.java:27)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191)
at org.springframework.security.web.debug.DebugFilter.invokeWithWrappedRequest(DebugFilter.java:90)
at org.springframework.security.web.debug.DebugFilter.doFilter(DebugFilter.java:78)
at org.springframework.security.web.debug.DebugFilter.doFilter(DebugFilter.java:67)
at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:195)
at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74)
at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:230)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:384)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:904)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
at java.base/java.lang.Thread.run(Thread.java:840)
2024-09-22 15:31:39,948 DEBUG [http-nio-8080-exec-1] o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver: Using @ExceptionHandler org.example.controller.ExceptionController#handleSampleException(HttpServletRequest, Exception)
2024-09-22 15:31:39,950 DEBUG [http-nio-8080-exec-1] o.e.c.ExceptionController: SampleException: setup error page for does-not-exist does not exist! Big surprise!
2024-09-22 15:31:39,958 WARN [http-nio-8080-exec-1] o.s.w.s.h.AbstractHandlerExceptionResolver: Resolved [org.example.controller.SampleException: does-not-exist does not exist! Big surprise!]
它停在那里并且没有呈现错误页面。
整个示例都可以运行
$ get clone https://github.com/smaring/spring-servlet-filter-exception.git
$ cd spring-servlet-filter-exception
$ mvn clean spring-boot:run
并打开浏览器访问http://localhost:8080/does-not-exist
感谢您的评论m-deinum。 它为我指明了正确的方向。
我尝试了SimpleMappingExceptionResolver和各种HandlerExceptionResolver策略,但没有成功。 当我意识到我唯一的希望是启动过滤器链的响应时,我想出了一个适合我的想法。 在我的过滤器中,我捕获抛出的异常,我传递了 SpringTemplateEngine,以便我可以从那里渲染我的 Thymeleaf 页面。
所以,我的异常处理过滤器就变成了
@Slf4j
public class FilterChainExceptionHandlerFilter extends OncePerRequestFilter {
private final SpringTemplateEngine templateEngine;
public FilterChainExceptionHandlerFilter( SpringTemplateEngine templateEngine ) {
this.templateEngine = templateEngine;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (Exception e) {
log.error("caught exception ", e);
if ( e instanceof SampleException ) {
Context context = new Context();
context.setVariable("statusCode", "123");
context.setVariable("message", e.getLocalizedMessage() );
String htmlTemplate = this.templateEngine.process("error", context);
PrintWriter printWriter = response.getWriter();
printWriter.println( htmlTemplate );
printWriter.close();
}
}
}
}
和我的安全过滤器链
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
@Autowired
private SpringTemplateEngine templateEngine;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.anonymous(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.addFilterBefore( new FilterChainExceptionHandlerFilter( templateEngine ), DisableEncodeUrlFilter.class )
.addFilterBefore( new SampleFilter(), SecurityContextHolderFilter.class )
.build();
}
}
这正是我的目标。
我已经在 Github 上更新了示例项目,适合任何想要从控制器或过滤器对 Thymeleaf 页面进行异常处理的简单示例的人。