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

SpringBoot全局异常处理器无法捕获JwtTokenProvider抛出的异常

问题排查:SpringBoot全局异常处理器无法捕获JWT过期异常

问题描述

开发SpringBoot应用时采用JWT实现安全验证,用户注册场景下抛出的UserAlreadyRegisterException可以被全局异常处理器捕获,并返回如下规范响应:

{
    "timeStamp": "2024-09-27T04:07:37.495+00:00",
    "code": "p-500",
    "message": "El usuario ya se encuentra registrado en la aplicacion",
    "url": "/api/v1/auth/register"
}

但在处理其他端点的过期Token时,JwtTokenProvider中抛出的JwtTokenExpiredException无法被全局异常处理器捕获,仅会在控制台输出异常信息。

相关代码

用户注册服务代码

public void registrarUsuario(RegisterDTO registerDTO){
    if (authRepository.existsByEmail(registerDTO.getEmail())){
        throw new UserAlreadyRegisterException();
    }
    Usuarios usuario = new Usuarios();
    usuario.setPrimer_nombre(registerDTO.getPrimer_nombre());
    usuario.setSegundo_nombre(registerDTO.getSegundo_nombre());
    usuario.setPrimer_apellido(registerDTO.getPrimer_apellido());
    usuario.setSegundo_apellido(registerDTO.getSegundo_apellido());
    usuario.setEmail(registerDTO.getEmail());
    usuario.setTelefono(registerDTO.getTelefono());
    Credenciales credencial = new Credenciales();
    credencial.setContraseña(passwordEncoder.encode(registerDTO.getContraseña()));
    credencialesRepository.save(credencial);
    usuario.setCredenciales(credencial);
    Roles roles = rolRepository.findByNombre("usuario").orElseThrow(() -> new NoSuchElementException("No se encontro el rol usuarios"));
    usuario.setRoles(Collections.singletonList(roles));
    authRepository.save(usuario);
}

全局异常处理器代码

package com.api.reservavuelos.Exceptions;

import com.api.reservavuelos.DTO.ResponseExceptionDTO;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Date;

@RestControllerAdvice
public class GlobalExceptionHandler {
    private Date tiempoactual = new Date();

    @ExceptionHandler(JwtTokenExpiredException.class)
    public ResponseEntity<ResponseExceptionDTO> handleJwtTokenExpiredException(HttpServletRequest request, JwtTokenExpiredException exception){
    return new ResponseEntity<>(new ResponseExceptionDTO(tiempoactual,"P-401", exception.getMessage(), request.getRequestURI()), HttpStatus.UNAUTHORIZED);
    }

    @ExceptionHandler(AuthenticationCredentialsNotFoundException.class)
    public ResponseEntity<ResponseExceptionDTO> handleAuthenticationCredentialsNotFoundException(HttpServletRequest request , AuthenticationCredentialsNotFoundException exception){
        return new ResponseEntity<>(new ResponseExceptionDTO(tiempoactual,"P-401", exception.getMessage(), request.getRequestURI()), HttpStatus.UNAUTHORIZED);
    }

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ResponseExceptionDTO> handleUserNotFoundException(HttpServletRequest request, UserNotFoundException exception) {
        return new ResponseEntity<>(new ResponseExceptionDTO(tiempoactual,"P-404", exception.getMessage(), request.getRequestURI()), HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(UserAlreadyRegisterException.class)
    public ResponseEntity<ResponseExceptionDTO> handleUserAlreadyRegisterException(HttpServletRequest request, UserAlreadyRegisterException exception) {
        return new ResponseEntity<>(new ResponseExceptionDTO(tiempoactual,"p-500", exception.getMessage(), request.getRequestURI()), HttpStatus.BAD_REQUEST);
    }

}

JwtTokenProvider代码

package com.api.reservavuelos.Security;

import com.api.reservavuelos.Exceptions.JwtTokenExpiredException;
import com.api.reservavuelos.Exceptions.UserAlreadyRegisterException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JwtTokenProvider {

    public String generateToken(Authentication authentication) {
        String username = authentication.getName();
        Date tiempoactual = new Date();
        Date expiracion = new Date(tiempoactual.getTime() + ConstantSecurity.JWT_EXPIRATION_TOKEN);

        String Token = Jwts.builder()
                .setSubject(username)
                .setIssuedAt(tiempoactual)
                .setExpiration(expiracion)
                .signWith(SignatureAlgorithm.HS512, ConstantSecurity.JWT_FIRMA)
                .compact();

        return Token;
    }

    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(ConstantSecurity.JWT_FIRMA)
                .build()
                .parseClaimsJws(token)
                .getBody();

      return claims.getSubject();
    }

    public Boolean IsValidToken(String token) {
        try {
           Jwts.parser()
           .setSigningKey(ConstantSecurity.JWT_FIRMA)
           .build()
           .parseClaimsJws(token);
           return true;
        }catch (ExpiredJwtException e) {
         throw new JwtTokenExpiredException("Jwt ha expirado");
        } catch (AuthenticationCredentialsNotFoundException e)  {
            throw new AuthenticationCredentialsNotFoundException("Jwt esta incorrecto");
        }
    }
}

核心原因分析

@RestControllerAdvice标注的全局异常处理器,仅能捕获Controller层及后续调用链中抛出的异常。而JWT验证逻辑通常是在**Spring Security过滤器(Filter)**中执行的,过滤器的执行时机早于DispatcherServlet(Spring MVC处理请求的入口),因此过滤器中抛出的异常无法进入全局异常处理器的处理范围,只能在控制台输出。

解决方案

方案1:在JWT过滤器内部捕获异常并返回响应

自定义JWT认证过滤器,在过滤器中直接捕获JwtTokenExpiredException等异常,手动构建规范响应写入HttpServletResponse:

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtTokenProvider jwtTokenProvider;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            String token = extractJwtFromRequest(request);
            if (token != null && jwtTokenProvider.IsValidToken(token)) {
                String username = jwtTokenProvider.getUsernameFromToken(token);
                // 构建Authentication并设置到SecurityContext
                // ...
            }
            filterChain.doFilter(request, response);
        } catch (JwtTokenExpiredException e) {
            buildErrorResponse(response, request, HttpStatus.UNAUTHORIZED, "P-401", e.getMessage());
        } catch (AuthenticationCredentialsNotFoundException e) {
            buildErrorResponse(response, request, HttpStatus.UNAUTHORIZED, "P-401", e.getMessage());
        }
    }

    private String extractJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }

    private void buildErrorResponse(HttpServletResponse response, HttpServletRequest request, HttpStatus status, String code, String message) throws IOException {
        response.setStatus(status.value());
        response.setContentType("application/json");
        ResponseExceptionDTO errorDTO = new ResponseExceptionDTO(new Date(), code, message, request.getRequestURI());
        response.getWriter().write(objectMapper.writeValueAsString(errorDTO));
    }
}

方案2:通过Spring Security的AuthenticationEntryPoint统一处理

配置自定义的AuthenticationEntryPoint,处理认证阶段的异常:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    private final JwtTokenProvider jwtTokenProvider;

    public SecurityConfig(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/v1/auth/**").permitAll()
                        .anyRequest().authenticated())
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(ex -> ex.authenticationEntryPoint(new CustomAuthEntryPoint()));
        return http.build();
    }

    static class CustomAuthEntryPoint implements AuthenticationEntryPoint {
        private final ObjectMapper objectMapper = new ObjectMapper();

        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
            ResponseExceptionDTO errorDTO;
            if (authException.getCause() instanceof JwtTokenExpiredException) {
                errorDTO = new ResponseExceptionDTO(new Date(), "P-401", "Jwt ha expirado", request.getRequestURI());
            } else {
                errorDTO = new ResponseExceptionDTO(new Date(), "P-401", authException.getMessage(), request.getRequestURI());
            }
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setContentType("application/json");
            response.getWriter().write(objectMapper.writeValueAsString(errorDTO));
        }
    }
}

额外优化点

全局异常处理器中tiempoactual是类初始化时创建的固定Date对象,会导致所有异常响应的时间都是应用启动时间,建议在每个异常处理方法中创建新的Date实例:

@ExceptionHandler(UserAlreadyRegisterException.class)
public ResponseEntity<ResponseExceptionDTO> handleUserAlreadyRegisterException(HttpServletRequest request, UserAlreadyRegisterException exception) {
    return new ResponseEntity<>(new ResponseExceptionDTO(new Date(),"p-500", exception.getMessage(), request.getRequestURI()), HttpStatus.BAD_REQUEST);
}

内容的提问来源于stack exchange,提问作者Wuubzi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 20:05:08