Spring Boot 3中如何区分处理认证异常与HTTP 500类异常
问题
使用Spring Boot 3.0.1结合Spring Security构建API时,已配置安全过滤器链并自定义AuthenticationEntryPoint处理认证相关异常(如JWT令牌无效),但该处理器会默认处理所有其他异常(如请求参数无效),导致无效请求返回401而非500内部服务器错误。如何让自定义处理器仅处理认证异常?
已尝试配置.requestMatchers("/error**").permitAll(),但所有错误都返回500,包括认证问题。
现有配置代码
SecurityFilterChain配置
@Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable) .exceptionHandling( exception -> exception.authenticationEntryPoint(unauthorizedEntryPoint) ) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/taskservice/api/v1/auth/**").permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .anyRequest().authenticated() ) .sessionManagement(manager -> manager.sessionCreationPolicy(STATELESS)) .authenticationProvider(authenticationProvider()) .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class) ; return http.build(); }
自定义AuthenticationEntryPoint
@Component @Slf4j public class Http401UnauthorizedEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { log.error("Unauthorized error: {}", authException.getMessage()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); LoginResponseError body = LoginResponseError.builder() .status(HttpServletResponse.SC_UNAUTHORIZED) .error("Unauthorized") .timestamp(Instant.now()) .message(authException.getMessage()) .path(request.getServletPath()) .build(); final ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,false); mapper.writeValue(response.getOutputStream(), body); } }
JWT认证过滤器
@Override protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain ) throws ServletException, IOException { // try to get JWT in cookie or in Authorization Header String jwt = jwtService.getJwtFromCookies(request); final String authHeader = request.getHeader("Authorization"); if((jwt == null && (authHeader == null || !authHeader.startsWith("Bearer "))) || request.getRequestURI().contains("/auth")){ filterChain.doFilter(request, response); return; } // If the JWT is not in the cookies but in the "Authorization" header if (jwt == null && authHeader.startsWith("Bearer ")) { jwt = authHeader.substring(7); // after "Bearer " } final String userEmail = jwtService.extractUserName(jwt); if(StringUtils.isNotEmpty(userEmail) && SecurityContextHolder.getContext().getAuthentication() == null){ UserDetails userDetails = this.employeeUserDetailsService.loadUserByUsername(userEmail); if(jwtService.isTokenValid(jwt, userDetails)){ SecurityContext context = SecurityContextHolder.createEmptyContext(); UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); context.setAuthentication(authToken); SecurityContextHolder.setContext(context); } } filterChain.doFilter(request,response); }
解决方案
问题根源在于Spring Security的AuthenticationEntryPoint会在未认证的请求试图访问受保护资源时触发,但如果后续业务层抛出非认证异常,Spring Boot默认的错误处理机制被覆盖,导致异常被错误路由到AuthenticationEntryPoint。需通过以下步骤解决:
1. 配置全局异常处理器处理非认证异常
创建@RestControllerAdvice类,专门捕获业务、参数校验类异常,返回对应HTTP状态码:
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { // 处理请求参数无效、绑定错误等异常 @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidationExceptions(MethodArgumentNotValidException ex, HttpServletRequest request) { log.error("Validation error: {}", ex.getMessage()); ErrorResponse error = ErrorResponse.builder() .status(HttpServletResponse.SC_BAD_REQUEST) .error("Bad Request") .timestamp(Instant.now()) .message("请求参数无效") .path(request.getServletPath()) .build(); return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); } // 处理其他所有非认证异常 @ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponse> handleGenericExceptions(Exception ex, HttpServletRequest request) { log.error("Internal server error: {}", ex.getMessage(), ex); ErrorResponse error = ErrorResponse.builder() .status(HttpServletResponse.SC_INTERNAL_SERVER_ERROR) .error("Internal Server Error") .timestamp(Instant.now()) .message("服务器内部错误") .path(request.getServletPath()) .build(); return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); } } // 可复用的错误响应DTO @Data @Builder public class ErrorResponse { private int status; private String error; private Instant timestamp; private String message; private String path; }
2. 修改SecurityFilterChain,放行错误端点
确保Spring Boot默认的/error端点允许匿名访问,避免非认证异常被Security拦截:
@Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable) .exceptionHandling( exception -> exception.authenticationEntryPoint(unauthorizedEntryPoint) ) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/taskservice/api/v1/auth/**").permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers("/error").permitAll() // 放行error端点 .anyRequest().authenticated() ) .sessionManagement(manager -> manager.sessionCreationPolicy(STATELESS)) .authenticationProvider(authenticationProvider()) .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class) ; return http.build(); }
3. 修正JWT过滤器的异常处理
在令牌校验失败时主动抛出AuthenticationException,确保认证异常被正确路由到自定义EntryPoint:
@Override protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain ) throws ServletException, IOException { String jwt = jwtService.getJwtFromCookies(request); final String authHeader = request.getHeader("Authorization"); if((jwt == null && (authHeader == null || !authHeader.startsWith("Bearer "))) || request.getRequestURI().contains("/auth")){ filterChain.doFilter(request, response); return; } if (jwt == null && authHeader.startsWith("Bearer ")) { jwt = authHeader.substring(7); } try { final String userEmail = jwtService.extractUserName(jwt); if(StringUtils.isNotEmpty(userEmail) && SecurityContextHolder.getContext().getAuthentication() == null){ UserDetails userDetails = this.employeeUserDetailsService.loadUserByUsername(userEmail); if(!jwtService.isTokenValid(jwt, userDetails)){ // 令牌无效时抛出认证异常 throw new BadCredentialsException("Invalid JWT token"); } // 设置认证上下文 SecurityContext context = SecurityContextHolder.createEmptyContext(); UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); context.setAuthentication(authToken); SecurityContextHolder.setContext(context); } } catch (Exception ex) { // 将令牌解析/校验异常转为AuthenticationException if (!(ex instanceof AuthenticationException)) { throw new BadCredentialsException("Invalid JWT token", ex); } throw ex; } filterChain.doFilter(request,response); }
原理说明
AuthenticationEntryPoint仅在用户未认证访问受保护资源或**认证过程中抛出AuthenticationException**时触发。- 全局异常处理器
@RestControllerAdvice捕获所有非认证异常,返回对应状态码,不会被路由到AuthenticationEntryPoint。 - 放行
/error端点确保Spring Boot默认错误处理机制正常工作,避免非认证异常被Security拦截。
内容的提问来源于stack exchange,提问作者Maksim Artsishevskiy
相关产品推荐
相关产品推荐

