Обработка внутренней ошибки сервера, когда эмитент OAuth недоступен в WebFluxSecurityJAVA

Программисты JAVA общаются здесь
Anonymous
Обработка внутренней ошибки сервера, когда эмитент OAuth недоступен в WebFluxSecurity

Сообщение Anonymous »

Я не могу настроить текст ответа по умолчанию «500 внутренняя ошибка сервера», если эмитент (Keycloak) для моего реактивного сервера ресурсов Spring недоступен. Я хочу добавить собственный ответ JSON, но, например, обычный @ExceptionHandler не работает, потому что аутентификация не проходит. (Spring Security v6.2.0, Spring boot v3.2.0):

Код: Выделить всё

org.springframework.boot
spring-boot-starter-oauth2-resource-server


org.springframework.security
spring-security-oauth2-jose

Код: Выделить всё

application.yaml
( Issuer-uri намеренно указан неправильно: я хочу имитировать, что мой сервер аутентификации Keycloak недоступен):

Код: Выделить всё

spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${JWT_ISSUER_URI:http://notavailable.io/realms/my-realm}

Код: Выделить всё

SecurityConfig.java
:

Код: Выделить всё

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebFluxSecurity
public class SecurityConfiguration {

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http.authorizeExchange(exchanges -> exchanges.pathMatchers("/actuator/**")
.permitAll()
.anyExchange()
.hasAuthority("SCOPE_foobar"))
.oauth2ResourceServer(oAuth2ResourceServerSpec -> oAuth2ResourceServerSpec.jwt(withDefaults())
.authenticationEntryPoint((webExchance, exception) -> handleAuthError(webExchance))
.accessDeniedHandler((webExchance, exception) -> handleAuthError(webExchance)))
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.build();
}

/**
* @SO: This handle is called if the authentication fails with 401 or 403.  But it's not called if an 500 internal
*      server error is thrown.
*/
private Mono handleAuthError(ServerWebExchange webExchance) {
final ServerHttpResponse response = webExchance.getResponse();
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
response.getHeaders().setContentType(APPLICATION_JSON);

final var msg = "{\"foo\":\"bar\"}".getBytes(StandardCharsets.UTF_8);
return response.writeWith(Mono.just(response.bufferFactory().wrap(msg)));
}
}

Если я отправлю запрос на свой сервер (есть контроллер /foobar), а Issuer-uri недоступен, сервер регистрирует следующую трассировку стека :

Код: Выделить всё

Error has been observed at the following site(s):
*__checkpoint ⇢ AuthenticationWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ ReactorContextWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ HttpHeaderWriterWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
*__checkpoint ⇢ HTTP GET "/foobar" [ExceptionHandlingWebHandler]
Original Stack Trace:
at org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager.onError(JwtReactiveAuthenticationManager.java:81)
at reactor.core.publisher.Mono.lambda$onErrorMap$27(Mono.java:3785)
at reactor.core.publisher.Mono.lambda$onErrorResume$29(Mono.java:3875)
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:94)
[...]
Caused by: org.springframework.security.oauth2.jwt.JwtException: An error occurred while attempting to decode the Jwt:
at org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.lambda$decode$2(NimbusReactiveJwtDecoder.java:171)
at reactor.core.publisher.Mono.lambda$onErrorMap$27(Mono.java:3785)
at reactor.core.publisher.Mono.lambda$onErrorResume$29(Mono.java:3875)
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:94)
[...]
Caused by: java.lang.IllegalArgumentException: Unable to resolve the Configuration with the provided Issuer of "http://notavailable.io/realms/foobar/realms/my-realm"
at org.springframework.security.oauth2.jwt.ReactiveJwtDecoderProviderConfigurationUtils.lambda$getConfiguration$8(ReactiveJwtDecoderProviderConfigurationUtils.java:139)
at reactor.core.publisher.Flux.lambda$onErrorMap$28(Flux.java:7239)
at reactor.core.publisher.Flux.lambda$onErrorResume$29(Flux.java:7292)
[...]
Caused by: org.springframework.web.reactive.function.client.WebClientRequestException: Failed to resolve 'notavailable.io' [A(1)] after 4 queries
at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.lambda$wrapException$9(ExchangeFunctions.java:136)
IllegalArgumentException создается в строке org.springframework.security.oauth2.jwt.ReactiveJwtDecoderProviderConfigurationUtils#getConfiguration (в строке onErrorMap), реализация которой выглядит так:

Код: Выделить всё

    private static Mono getConfiguration(String issuer, WebClient web, URI...  uris) {
String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " + "\"" + issuer + "\"";
return Flux.just(uris)
.concatMap((uri) -> web.get().uri(uri).retrieve().bodyToMono(STRING_OBJECT_MAP))
.flatMap((configuration) -> {
if (configuration.get("jwks_uri") == null) {
return Mono.error(() -> new IllegalArgumentException("The public JWK set URI must not be null"));
}
return Mono.just(configuration);
})
.onErrorContinue((ex) -> ex instanceof WebClientResponseException
&& ((WebClientResponseException) ex).getStatusCode().is4xxClientError(), (ex, object) -> {
})
.onErrorMap(RuntimeException.class,
(ex) -> (ex instanceof IllegalArgumentException) ? ex
: new IllegalArgumentException(errorMessage, ex))
.next()
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException(errorMessage)));
}
Фактическое поведение
Если эмитент недоступен, клиент получает внутреннюю ошибку сервера 500 ответ с телом:

Код: Выделить всё

{
"timestamp": "2024-06-21T16:18:13.190+00:00",
"path": "/foobar",
"status": 500,
"error": "Internal Server Error",
"requestId": "2eeaa482-2"
}
Желаемое поведение
Если эмитент недоступен, клиент получает внутреннюю ошибку сервера 500 ответ с телом:

Код: Выделить всё

{
"foo": "bar"
}
Я пробовал использовать @ExceptionHandler (не вызывается) и создать свой собственный JwtDecoder (не нашел метода Я мог бы переопределить, чтобы перехватить исключение и отправить ответ).
Кто-нибудь знает, как изменить ответ, когда URL-адрес эмитента недоступен?

Подробнее здесь: https://stackoverflow.com/questions/786 ... ebfluxsecu

Вернуться в «JAVA»