SecurityWebFilterChain中IllegalArgumentException异常处理失效问题
问题描述
我使用zalando problem spring webflux来简化异常处理。
@Configuration @Import(SecurityProblemSupport::class) class SecurityConfig{ @Autowired lateinit var problemSupport: SecurityProblemSupport //... @Bean fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain = http { //.... exceptionHandling { authenticationEntryPoint = problemSupport accessDeniedHandler = problemSupport } authorizeExchange { //... authorize(pathMatchers("/{id}/**"), customCheck()) //... } } fun customCheck():ReactiveAuthenticationManager<AuthenticationContext>{ // 此处抛出运行时IllegalArgumentException异常 } }
我已为IllegalArgumentException创建了异常处理器,它能正常处理控制器抛出的该异常。
预期行为
通过异常处理器(已将accessDeniedHandler配置为zalando全局问题处理器)处理IllegalArgumentException,返回400状态码。
实际行为
但实际返回的是500内部错误。
原因分析
Spring Security的过滤器链执行顺序早于WebFlux控制器,授权阶段抛出的IllegalArgumentException不会被全局的WebFlux异常处理器(包括Zalando的Problem处理器)捕获。你配置的accessDeniedHandler仅处理AccessDeniedException及其子类的授权拒绝场景,非Security标准异常会被默认逻辑转为500内部错误。
解决方案
方案1:扩展SecurityProblemSupport处理自定义异常
自定义SecurityProblemSupport子类,在其中捕获IllegalArgumentException并转换为400响应:
class CustomSecurityProblemSupport : SecurityProblemSupport() { override fun handle(exchange: ServerWebExchange, ex: AccessDeniedException): Mono<Void> { return if (ex.cause is IllegalArgumentException) { val badRequestProblem = Problem.builder() .withStatus(Status.BAD_REQUEST) .withTitle("非法请求") .withDetail((ex.cause as IllegalArgumentException).message) .build() writeProblem(exchange, badRequestProblem) } else { super.handle(exchange, ex) } } override fun onAuthenticationFailure(exchange: ServerWebExchange, ex: AuthenticationException): Mono<Void> { return if (ex.cause is IllegalArgumentException) { val badRequestProblem = Problem.builder() .withStatus(Status.BAD_REQUEST) .withTitle("认证失败") .withDetail((ex.cause as IllegalArgumentException).message) .build() writeProblem(exchange, badRequestProblem) } else { super.onAuthenticationFailure(exchange, ex) } } }
然后在SecurityConfig中替换导入的类:
@Configuration @Import(CustomSecurityProblemSupport::class) class SecurityConfig{ @Autowired lateinit var problemSupport: CustomSecurityProblemSupport // 其余代码不变 }
方案2:抛出Security标准异常并映射状态码
在自定义校验中抛出Spring Security的AuthorizationDeniedException(或自定义继承AccessDeniedException的异常),再通过Zalando处理器映射到400状态:
fun customCheck(): ReactiveAuthenticationManager<AuthenticationContext> { return ReactiveAuthenticationManager { context -> // 执行你的校验逻辑 if (校验不通过) { Mono.error(AuthorizationDeniedException("参数非法,拒绝访问")) } else { Mono.just(context.authentication) } } }
配置异常映射:
@Bean fun problemConfig(): ProblemHandling { return ProblemHandling.builder() .withExceptionMapping(AuthorizationDeniedException::class.java) { ex -> Problem.builder() .withStatus(Status.BAD_REQUEST) .withDetail(ex.message) .build() } .build() }
方案3:全局ErrorWebExceptionHandler接管所有异常
确保Zalando的ProblemErrorWebExceptionHandler能处理Security阶段的异常,可自定义全局异常处理器统一处理:
@Component class GlobalErrorWebExceptionHandler( problemProperties: ProblemProperties, messageSource: MessageSource, serverCodecConfigurer: ServerCodecConfigurer ) : ProblemErrorWebExceptionHandler(problemProperties, messageSource, serverCodecConfigurer) { override fun handle(exchange: ServerWebExchange, throwable: Throwable): Mono<Void> { val handledThrowable = if (throwable is IllegalArgumentException) { // 转换为Problem格式 Problem.builder() .withStatus(Status.BAD_REQUEST) .withDetail(throwable.message) .build() } else { throwable } return super.handle(exchange, handledThrowable) } }
内容的提问来源于stack exchange,提问作者Hantsy

