How to handle custom JwtAuthException Spring security

У меня есть JwtTokenProvider:

@Component
@PropertySource("classpath:jwt.properties")
@RequiredArgsConstructor
public class JwtTokenProvider {
//some code ...
   public boolean validateToken(String token) {
        try {
            Jws<Claims> claimsJws = Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(token);
            return !claimsJws.getBody().getExpiration().before(new Date());
        } catch (JwtException | IllegalArgumentException e) {
            throw new JwtAuthenticationException("JWT token expired or invalid!");
        }
    }
}

Это обычный компонент, который, получается, выбрасывает исключение, если токен "протух"

Понятно, что, если обратиться к серверу с попорченным токеном, он выбросит исключение - но с ошибкой 500. Как перехватить его так, чтобы можно было возвращать 401? И только конкретно это исключение, 403 мне необходимо сохранить

Вот некоторый сопутствующие классы :

  1. Фильтр
@RequiredArgsConstructor
public class JwtTokenFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        String token = jwtTokenProvider.retrieveToken((HttpServletRequest) servletRequest);

        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication authentication = jwtTokenProvider.getAuthentication(token);

            if (authentication != null) {
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

2.Конфиг

@RequiredArgsConstructor
public class JwtConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private final JwtTokenProvider provider;

    @Override
    public void configure(HttpSecurity builder) throws Exception {
        JwtTokenFilter filter = new JwtTokenFilter(provider);
        builder.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
    }
}
==========================
@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = {SecurityPackageMarker.class})
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtTokenProvider provider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors()
                .and()
                .addFilterBefore(new CorsFilter(corsConfigurationSource()), SessionManagementFilter.class)
                .httpBasic().disable()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/hello", "/login", "/register").permitAll()
                .anyRequest().authenticated()
                .and()
                .apply(new JwtConfigurer(provider));
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config.applyPermitDefaultValues());
        return source;
    }
}
  1. Exception:
public class JwtAuthenticationException extends AuthenticationException {
    public JwtAuthenticationException(String explanation) {
        super(explanation);
    }
}

4.Controller:

@PostMapping("/login")
    @ResponseStatus(HttpStatus.OK)
    public Properties login(@RequestBody AuthenticationInDto dto) {
        try {
            String username = dto.getUsername();
            String password= dto.getPassword();
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
            Optional<User> user = userService.findByUsername(username);

            if (!user.isPresent()) {
                throw new UsernameNotFoundException("User with username: " + username + " not found");
            }

            String token = provider.createToken(username, user.get().getRole());

            Properties response = new Properties();
            response.setProperty("jwtToken", token);

            return response;
        } catch (AuthenticationException e) {
            throw new BadCredentialsException(e.getMessage());
        }
    }

Но, тем не менее, он не перехватывает мое исключение(не пробрасывает BadCredentialsException) - сервер падает с ошибкой 500, хоть у меня и есть обработчик:

@RestControllerAdvice
public class ControllersExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(BadCredentialsException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ErrorDto handleBadCredentials(BadCredentialsException ex) {
        return new ErrorDto(HttpStatus.UNAUTHORIZED.value(),
                            ex.getMessage());
    }

    @ExceptionHandler(RegistrationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorDto handleRegistrationException(RegistrationException ex) {
        return new ErrorDto(HttpStatus.BAD_REQUEST.value(),
                            ex.getMessage());
    }
}

Ответы (1 шт):

Автор решения: lampa

Я упростил до любой ошибки в фильтре:

@Component
public class TokenFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        String token = servletRequest.getParameter("token");

        if (token == null || !token.equals("123456")) {
            HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
            httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value());
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }
}

В вашем случае поймать ошибку в фильтре и отправить sendError(). В sendError() вторым аргументом можно добавить описание ошибки.

Еще непонятно, каким образом у вас получилось скомпилировать, если в методе validateToken() исключение обрабатываемое, а интерфейс GenericFilterBean задает методу только 2 возможных исключения: IOException и ServletException.

А через @RestControllerAdvice поймать ошибку по идее не выйдет, т.к. servlet -> before filter -> spring mvc -> after filter.

Кстати через @Aspect вполне себе работает:

@Aspect
@Component
public class ExceptionHandlerAspect {

    @Before(value = "within(@org.springframework.web.bind.annotation.RestController *)")
    public void before(JoinPoint joinPoint) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        String token = request.getParameter("token");

        if (token == null || !token.equals("123456")) {
            throw new BadCredentialsException(HttpStatus.UNAUTHORIZED, "bad token");
        }
    }
}
→ Ссылка