Spring Security ExceptionTranslationFilter未处理自定义过滤器抛出的异常
配置Spring Security后,当JWT无效时parseAccessToken()会抛出继承自AuthenticationException的InvalidTokenException,但配置的JwtAuthenticationEntryPoint的commence方法并未执行,无相关日志输出。
相关代码
SecurityConfig配置
@Configuration @EnableWebSecurity @EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthFilter; private final JwtAuthenticationEntryPoint jwtFilterAuthenticationEntryPoint; private final AuthzAccessDeniedHandler authzAccessDeniedHandler; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers(USER_PATH + USER_ID_PATH_VAR).authenticated() .requestMatchers(HttpMethod.POST, EVENT_PATH).hasRole("ORGANIZER") .requestMatchers(HttpMethod.GET, TICKET_PATH).authenticated() .requestMatchers(AUTH_PATH + "/logout/{userUuid}").authenticated() .anyRequest().permitAll() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .cors(Customizer.withDefaults()) .exceptionHandling(handler -> handler .authenticationEntryPoint(jwtFilterAuthenticationEntryPoint) .accessDeniedHandler(authzAccessDeniedHandler) ) .csrf(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .logout(AbstractHttpConfigurer::disable) ; return http.build(); } //other omited code...
JwtAuthenticationFilter代码
@Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final String AUTHZ_HEADER_PREFIX = "Bearer "; private final JwtService jwtService; @Override protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain ) throws ServletException, IOException { String authzHeader = request.getHeader("Authorization"); if (authzHeader == null || !authzHeader.startsWith(AUTHZ_HEADER_PREFIX)) { filterChain.doFilter(request, response); return; } AccessToken accessToken = jwtService.parseAccessToken( authzHeader.substring(AUTHZ_HEADER_PREFIX.length()) ); authenticateIfCurrentlyNot(accessToken, request); filterChain.doFilter(request, response); } private void authenticateIfCurrentlyNot(AccessToken accessToken, HttpServletRequest request) { if (SecurityContextHolder.getContext().getAuthentication() == null) { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( accessToken.claims().sub(), null, List.of(convertRoleToSimpleGrantedAuthority( accessToken.claims().role() )) ); token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(token); } } private SimpleGrantedAuthority convertRoleToSimpleGrantedAuthority(UserRole role) { return new SimpleGrantedAuthority("ROLE_" + role.name()); } }
自定义异常与AuthenticationEntryPoint代码
public class InvalidTokenException extends AuthenticationException { public InvalidTokenException(String message) { super(message); } public static String errorCode() { return "invalid.token"; } } @Component @Slf4j public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Autowired @Qualifier("handlerExceptionResolver") private HandlerExceptionResolver resolver; @Override public void commence( HttpServletRequest request, HttpServletResponse response, AuthenticationException authException ) { log.error(authException.getMessage(), authException); resolver.resolveException(request, response, null, authException); } }
原因
ExceptionTranslationFilter仅会在后续过滤器或请求处理流程中抛出AuthenticationException时介入处理。而当前JwtAuthenticationFilter中直接抛出的InvalidTokenException会被Servlet容器的异常处理机制捕获,而非Spring Security的ExceptionTranslationFilter,因此AuthenticationEntryPoint不会被触发。
修复方案
方案一:在过滤器中捕获异常并手动触发AuthenticationEntryPoint
修改JwtAuthenticationFilter,注入JwtAuthenticationEntryPoint,捕获InvalidTokenException后直接调用其commence方法:
@Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final String AUTHZ_HEADER_PREFIX = "Bearer "; private final JwtService jwtService; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; // 新增注入 @Override protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain ) throws ServletException, IOException { String authzHeader = request.getHeader("Authorization"); if (authzHeader == null || !authzHeader.startsWith(AUTHZ_HEADER_PREFIX)) { filterChain.doFilter(request, response); return; } try { AccessToken accessToken = jwtService.parseAccessToken( authzHeader.substring(AUTHZ_HEADER_PREFIX.length()) ); authenticateIfCurrentlyNot(accessToken, request); filterChain.doFilter(request, response); } catch (InvalidTokenException e) { jwtAuthenticationEntryPoint.commence(request, response, e); } } // 其余代码不变... }
方案二:将异常绑定到请求,交由ExceptionTranslationFilter处理
在过滤器中捕获异常后,将其绑定到请求属性,让后续的ExceptionTranslationFilter识别并处理:
@Override protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain ) throws ServletException, IOException { String authzHeader = request.getHeader("Authorization"); if (authzHeader == null || !authzHeader.startsWith(AUTHZ_HEADER_PREFIX)) { filterChain.doFilter(request, response); return; } try { AccessToken accessToken = jwtService.parseAccessToken( authzHeader.substring(AUTHZ_HEADER_PREFIX.length()) ); authenticateIfCurrentlyNot(accessToken, request); } catch (InvalidTokenException e) { request.setAttribute("javax.servlet.error.exception", e); } filterChain.doFilter(request, response); }
额外检查
确认SecurityFilterChain中过滤器顺序正确:JwtAuthenticationFilter通过addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)添加,而ExceptionTranslationFilter默认在UsernamePasswordAuthenticationFilter之前,因此顺序无需调整。
内容的提问来源于stack exchange,提问作者momopoi

