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
相关产品推荐
相关产品推荐

