Как поймать и обработать InvalidGrantException (пользователь отключен)?

Я заметил, что мой ResponseEntityExceptionHandler не работает с исключениями, создаваемыми Spring Security в моем приложении Spring Boot.

Однако мне нужен способ поймать InvalidGrantException, который, кажется, выкидывается, когда учетная запись пользователя все еще отключена.

Сценарий использования прост: если пользователь в настоящее время отключен, я хочу выдать соответствующую ошибку моему клиенту s.t. он может отображать соответствующее сообщение.

Прямо сейчас ответ Spring Security:

{
  error: "invalid_grant", 
  error_description: "User is disabled"
}

Я видел этот вопрос, но по какой-то причине мой AuthFailureHandler не запускается:

@Configuration
@EnableResourceServer
public class ResourceServer extends ResourceServerConfigurerAdapter {

    @Component
    public class AuthFailureHandler implements AuthenticationEntryPoint {

        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

            // Never reached ..
            System.out.println("Hello World!");
        }
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()                        
            .authenticationEntryPoint(customAuthEntryPoint());;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.authenticationEntryPoint(customAuthEntryPoint());
    }

    @Bean
    public AuthenticationEntryPoint customAuthEntryPoint(){
        return new AuthFailureHandler();
    }

}

Есть идеи, что мне не хватает?


Код конфигурации

Вот мой OAuth2Configuration, который также содержит ResourceServerConfiguraerAdapter, который должен обрабатывать исключение.

@Configuration
@EnableAuthorizationServer
@EnableResourceServer
public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {

    private final TokenStore tokenStore;

    private final JwtAccessTokenConverter accessTokenConverter;

    private final AuthenticationManager authenticationManager;

    @Autowired
    public OAuth2Configuration(TokenStore tokenStore, JwtAccessTokenConverter accessTokenConverter, AuthenticationManager authenticationManager) {
        this.tokenStore = tokenStore;
        this.accessTokenConverter = accessTokenConverter;
        this.authenticationManager = authenticationManager;
    }

    @Value("${security.jwt.client-id}")
    private String clientId;

    @Value("${security.jwt.client-secret}")
    private String clientSecret;

    @Value("${security.jwt.scope-read}")
    private String scopeRead;

    @Value("${security.jwt.scope-write}")
    private String scopeWrite;

    @Value("${security.jwt.resource-ids}")
    private String resourceIds;

    private final static String WEBHOOK_ENDPOINTS = "/r/api/*/webhooks/**";

    private final static String PUBLIC_ENDPOINTS = "/r/api/*/public/**";

    @Override
    public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
        configurer
                .inMemory()
                .withClient(clientId)
                .secret(clientSecret)
                .scopes(scopeRead, scopeWrite)
                .resourceIds(resourceIds)
                .accessTokenValiditySeconds(60*60*24*7 * 2) // Access tokens last two weeks
                .refreshTokenValiditySeconds(60*60*24*7 * 12) // Refresh tokens last 12 weeks
                //.accessTokenValiditySeconds(5)
                //.refreshTokenValiditySeconds(10)
                .authorizedGrantTypes("password", "refresh_token"); //, "client_credentials");
    }


    /**
     * Since there are currently multiply clients, we map the OAuth2 endpoints under /api
     * to avoid conflicts with @{@link ForwardController}
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        enhancerChain.setTokenEnhancers(Collections.singletonList(accessTokenConverter));

        endpoints.tokenStore(tokenStore)

                // Create path mappings to avoid conflicts with forwarding controller

                .pathMapping("/oauth/authorize", "/api/v1/oauth/authorize")
                .pathMapping("/oauth/check_token", "/api/v1/oauth/check_token")
                .pathMapping("/oauth/confirm_access", "/api/v1/oauth/confirm_access")
                .pathMapping("/oauth/error", "/api/v1/oauth/error")
                .pathMapping("/oauth/token", "/api/v1/oauth/token")

                .accessTokenConverter(accessTokenConverter)
                .tokenEnhancer(enhancerChain)
                .reuseRefreshTokens(false)
                .authenticationManager(authenticationManager);
    }

    /**
     * Forwarding controller.
     *
     * This controller manages forwarding in particular for the static web clients. Since there are multiple
     * clients, this controller will map <i>any</i> GET request to the root /* to one of the clients.
     *
     * If no match was found, the default redirect goes to /web/index.html
     *
     */
    @Controller
    public class ForwardController {

        @RequestMapping(value = "/sitemap.xml", method = RequestMethod.GET)
        public String redirectSitemapXml(HttpServletRequest request) {
            return "forward:/a/web/assets/sitemap.xml";
        }

        @RequestMapping(value = "/robots.txt", method = RequestMethod.GET)
        public String redirectRobotTxt(HttpServletRequest request) {
            return "forward:/a/web/assets/robots.txt";
        }

        @RequestMapping(value = "/*", method = RequestMethod.GET)
        public String redirectRoot(HttpServletRequest request) {
            return "forward:/a/web/index.html";
        }

        @RequestMapping(value = "/a/**/{path:[^.]*}", method = RequestMethod.GET)
        public String redirectClients(HttpServletRequest request) {

            String requestURI = request.getRequestURI();

            if (requestURI.startsWith("/a/admin/")) {
                return "forward:/a/admin/index.html";
            }

            if (requestURI.startsWith("/a/swagger/")) {
                return "forward:/a/swagger/swagger-ui.html#/";
            }

            return "forward:/a/web/index.html";
        }

    }

    @Configuration
    @EnableResourceServer
    public class ResourceServer extends ResourceServerConfigurerAdapter {

        @Override
        public void configure(HttpSecurity http) throws Exception {

            // @formatter:off
            http
                    .requiresChannel()
                        /* Require HTTPS evereywhere*/
                        .antMatchers("/**")
                            .requiresSecure()
                    .and()
                        .exceptionHandling()
                    .and()
                        /* Permit all requests towards the public api as well as webhook endpoints. */
                        .authorizeRequests()
                            .antMatchers(PUBLIC_ENDPOINTS, WEBHOOK_ENDPOINTS)
                            .permitAll()
                        /* Required for ForwardController */
                        .antMatchers(HttpMethod.GET, "/*")
                            .permitAll()
                        .antMatchers("/r/api/**")
                            .authenticated();
            // @formatter:on
        }

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources
                    .resourceId(resourceIds)
                    .authenticationEntryPoint(customAuthEntryPoint());
        }

        @Component
        public class AuthFailureHandler implements AuthenticationEntryPoint {

            @Override
            public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                System.out.println("Hello");
                // FIXME We need to return HTTP 401 (s.t. the client knows what's going on) but for some reason it's not working as intended!
                throw authException;
            }
        }

        @Bean
        public AuthenticationEntryPoint customAuthEntryPoint(){
            return new AuthFailureHandler();
        }

    }

}

Кроме того, вот и WebSecurityConfigurerAdapter, хотя я не думаю, что это играет здесь роль:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Value("${security.signing-key}")
    private String signingKey;

    private final UserDetailsService userDetailsService;

    private final DataSource dataSource;

    @Autowired
    public WebSecurityConfig(@Qualifier("appUserDetailsService") UserDetailsService userDetailsService, DataSource dataSource) {
        this.userDetailsService = userDetailsService;
        this.dataSource = dataSource;
    }

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {

    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(signingKey);
        return converter;
    }

    /**
     * Using {@link JwtTokenStore} for JWT access tokens.
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }

    /**
     * Provide {@link DefaultTokenServices} using the {@link JwtTokenStore}.
     * @return
     */
    @Bean
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        return defaultTokenServices;
    }

    /**
     * We provide the AuthenticationManagerBuilder using our {@link UserDetailsService} and the {@link BCryptPasswordEncoder}.
     * @param auth
     * @throws Exception
     */
    @Autowired
    public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(this.userDetailsService)
                .passwordEncoder(passwordEncoder())
                .and()
                .authenticationProvider(daoAuthenticationProvider());
    }

    /**
     * Using {@link BCryptPasswordEncoder} for user-password encryption.
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * Provide {@link DaoAuthenticationProvider} for password encoding and set the {@link UserDetailsService}.
     * @return
     */
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        daoAuthenticationProvider.setUserDetailsService(this.userDetailsService);
        return daoAuthenticationProvider;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requiresChannel()
                .requestMatchers(r -> r.getHeader("X-Forwarded-Proto") != null)
                .requiresSecure();
    }   

}

Посмотрим, поможет ли это? github.com/t-less/sampleRegistration/blob/…

Tarun Lalwani 13.07.2019 17:43

не могли бы вы поделиться репо с базовым кодом, чтобы я мог его воспроизвести? потому что я не могу его воспроизвести.

Jonathan JOhx 14.07.2019 04:54

@JonathanJohx Я добавил соответствующий код в свой вопрос. Это будет вся моя конфигурация OAuth2 и WebSecurity.

Stefan Falk 14.07.2019 07:22
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Версия Java на основе версии загрузки
Версия Java на основе версии загрузки
Если вы зайдете на официальный сайт Spring Boot , там представлен start.spring.io , который упрощает создание проектов Spring Boot, как показано ниже.
Документирование API с помощью Swagger на Springboot
Документирование API с помощью Swagger на Springboot
В предыдущей статье мы уже узнали, как создать Rest API с помощью Springboot и MySql .
4
3
1 317
2

Ответы 2

Создайте собственный класс, расширяющий ResponseEntityExceptionHandler, аннотируйте его с помощью @ControllerAdvice и обработайте OAuth2Exception (базовый класс InvalidGrantException).

@ExceptionHandler({OAuth2Exception.class})
public ResponseEntity<Object> handleOAuth2Exception(OAuth2Exception exception, WebRequest request) {
    LOGGER.debug("OAuth failed on request processing", exception);
    return this.handleExceptionInternal(exception, ErrorOutputDto.create(exception.getOAuth2ErrorCode(), exception.getMessage()), new HttpHeaders(), HttpStatus.valueOf(exception.getHttpErrorCode()), request);
}

Используйте HandlerExceptionResolverComposite для объединения всех преобразователей исключений в системе в один преобразователь исключений. Это отменяет соответствующий bean-компонент, определенный в WebMvcConfigurationSupport класс. Добавьте список exceptionResolvers, например DefaultErrorAttributes и ExceptionHandlerExceptionResolver (это позволяет обрабатывать исключения на основе AOP путем включения классов с аннотацией @ControllerAdvice, таких как созданный нами пользовательский класс, расширяющий ResponseEntityExceptionHandler.

<bean id = "handlerExceptionResolver" class = "org.springframework.web.servlet.handler.HandlerExceptionResolverComposite">
<property name = "exceptionResolvers">
    <list>
        <bean class = "org.springframework.boot.web.servlet.error.DefaultErrorAttributes"/>

        <bean class = "org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver">
            <property name = "messageConverters">
                <list>
                    <ref bean = "jackson2HttpMessageConverter" />
                </list>
            </property>
        </bean>
    </list>
</property>

Вы пробовали, если определение @ControllerAdvice, указав, что ваш InvalidGrantException работает?

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    @ExceptionHandler(InvalidGrantException.class)
    public ResponseEntity<CustomErrorMessageTO> handleInvalidGrant(
            InvalidGrantException invalidGrantException) {

        CustomErrorMessageTO customErrorMessageTO = new CustomErrorMessageTO("Not granted or whatsoever");

        return new ResponseEntity<>(customErrorMessageTO, HttpStatus.UNAUTHORIZED);
    }
}

class CustomErrorMessageTO {

    private String message;

    public CustomErrorMessageTO(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

Другие вопросы по теме