You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 21:23:11