In spring-security-oauth2:2.4.0.RELEASE Klassen wie z OAuth2RestTemplate, OAuth2ProtectedResourceDetails Und ClientCredentialsAccessTokenProvider wurden alle als veraltet markiert.
Vom Javadoc zu diesen Klassen zeigt es auf a Spring Security-Migrationsleitfaden das unterstellt, dass die Leute zum Kernprojekt spring-security 5 migrieren sollten. Ich habe jedoch Probleme zu finden, wie ich meinen Anwendungsfall in diesem Projekt implementieren würde.
Alle Dokumentationen und Beispiele beziehen sich auf die Integration mit einem Drittanbieter-OAuth-Anbieter, wenn Sie möchten, dass eingehende Anfragen an Ihre Anwendung authentifiziert werden, und Sie den Drittanbieter-OAuth-Anbieter verwenden möchten, um die Identität zu überprüfen.
In meinem Anwendungsfall möchte ich nur eine Anfrage mit a stellen RestTemplate an einen externen Dienst, der durch OAuth geschützt ist. Aktuell erstelle ich eine OAuth2ProtectedResourceDetails mit meiner Client-ID und meinem Geheimnis, die ich in eine übergebe OAuth2RestTemplate. Ich habe auch einen Brauch ClientCredentialsAccessTokenProvider hinzugefügt OAuth2ResTemplate Dadurch werden der Token-Anforderung nur einige zusätzliche Header hinzugefügt, die von dem von mir verwendeten OAuth-Anbieter benötigt werden.
In der Spring-Security-5-Dokumentation habe ich einen Abschnitt gefunden, der erwähnt Anpassen der Token-Anfrage, aber auch das scheint im Zusammenhang mit der Authentifizierung einer eingehenden Anfrage bei einem OAuth-Drittanbieter zu stehen. Es ist nicht klar, wie Sie dies in Kombination mit etwas wie a verwenden würden ClientHttpRequestInterceptor um sicherzustellen, dass jede ausgehende Anfrage an einen externen Dienst zuerst ein Token erhält und dieses dann der Anfrage hinzugefügt wird.
Auch im oben verlinkten Migrationsleitfaden findet sich ein Hinweis auf a OAuth2AuthorizedClientService was angeblich für die Verwendung in Abfangjägern nützlich ist, aber auch dies sieht so aus, als ob es auf Dingen wie dem beruht ClientRegistrationRepository Dort werden anscheinend Registrierungen für Drittanbieter verwaltet, wenn Sie diese Bereitstellung verwenden möchten, um sicherzustellen, dass eine eingehende Anfrage authentifiziert wird.
Gibt es eine Möglichkeit, die neue Funktionalität in Spring-Security 5 zum Registrieren von OAuth-Anbietern zu nutzen, um ein Token zu erhalten, das ich ausgehenden Anforderungen meiner Anwendung hinzufügen kann?
Anar Sultanow
OAuth 2.0-Clientfunktionen von Spring Security 5.2.x werden nicht unterstützt RestTemplateaber nur WebClient. Sehen Spring-Sicherheitsreferenz:
HTTP-Client-Unterstützung
WebClient Integration für Servlet-Umgebungen (zum Anfordern geschützter Ressourcen)
Zusätzlich, RestTemplate wird in einer zukünftigen Version veraltet sein. Sehen RestTemplate javadoc:
NOTIZ: Ab 5.0 die nicht blockierende, reaktive org.springframework.web.reactive.client.WebClient bietet eine moderne Alternative zum RestTemplate mit effizienter Unterstützung für synchrone und asynchrone sowie Streaming-Szenarien. Der RestTemplate wird in einer zukünftigen Version veraltet sein und es werden in Zukunft keine größeren neuen Funktionen hinzugefügt. Siehe die WebClient Abschnitt der Spring Framework-Referenzdokumentation für weitere Details und Beispielcode.
Daher wäre die beste Lösung, aufzugeben RestTemplate zugunsten WebClient.
Verwenden WebClient für Client-Anmeldedatenfluss
Konfigurieren Sie die Clientregistrierung und den Anbieter entweder programmgesteuert oder mithilfe der automatischen Spring Boot-Konfiguration:
Konfigurieren Sie die WebClient Instanz zu verwenden ServerOAuth2AuthorizedClientExchangeFilterFunction mit dem bereitgestellten OAuth2AuthorizedClientManager:
Wenn Sie jetzt versuchen, eine Anfrage damit zu stellen WebClient Beispielsweise fordert er zunächst ein Token vom Autorisierungsserver an und schließt es in die Anforderung ein.
Das ist jetzt zu veraltet, lol … zumindest ist UnAuthenticatedServerOAuth2AuthorizedClientRepository …
– Vorschlaghammer
13. März 2020 um 22:06 Uhr
@AnarSultanov “Daher wäre die beste Lösung, RestTemplate zugunsten von WebClient aufzugeben.” Was ist mit Orten, an denen dies keine Option ist? Spring Cloud Discovery-, Configuration- und Feign-Clients verlassen sich beispielsweise immer noch auf RestTemplate und Dokumentationsstatus, um ein benutzerdefiniertes RestTemplate bereitzustellen, wenn Sie planen, diesen Diensten Sicherheit wie OAuth hinzuzufügen.
– lösak
4. April 2020 um 3:38 Uhr
@AnarSultanov Ich habe genau das Beispiel ausprobiert, das Sie gegeben haben, und ich erhalte einen 401-Fehler. Es scheint, dass es sich nicht authentifiziert, während versucht wird, Anforderungen auszuführen. Irgendwelche Tipps dazu?
– Rafael Braga
24. April 2020 um 1:08 Uhr
@rafael.braga Ich kann nichts empfehlen, ohne den gesamten Code und die gesamte Konfiguration zu sehen. Sie können das Beispiel aus dem offiziellen Repository ausprobieren und an Ihre Bedürfnisse anpassen: github.com/spring-projects/spring-security/tree/master/samples/…
– Anar Sultanow
24. April 2020 um 8:06 Uhr
Hier ist das relevante Spring Security-Dokument. Bietet etwas mehr Details und Erläuterungen zu den verschiedenen Möglichkeiten, wie Sie WebClient konfigurieren können: docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/…
– Crafton
16. Juli 2021 um 0:33 Uhr
Leandro Ass
Hallo, vielleicht ist es zu spät, aber RestTemplate wird in Spring Security 5 noch unterstützt, um nicht reaktive App RestTemplate wird immer noch verwendet, was Sie tun müssen, ist nur Spring Security richtig zu konfigurieren und einen Interceptor zu erstellen, wie im Migrationsleitfaden erwähnt
Verwenden Sie die folgende Konfiguration, um den Fluss „client_credentials“ zu verwenden
@Configuration
@RequiredArgsConstructor
public class OAuthRestTemplateConfig {
public static final String OAUTH_WEBCLIENT = "OAUTH_WEBCLIENT";
private final RestTemplateBuilder restTemplateBuilder;
private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
private final ClientRegistrationRepository clientRegistrationRepository;
@Bean(OAUTH_WEBCLIENT)
RestTemplate oAuthRestTemplate() {
var clientRegistration = clientRegistrationRepository.findByRegistrationId(Constants.OKTA_AUTH_SERVER_ID);
return restTemplateBuilder
.additionalInterceptors(new OAuthClientCredentialsRestTemplateInterceptorConfig(authorizedClientManager(), clientRegistration))
.setReadTimeout(Duration.ofSeconds(5))
.setConnectTimeout(Duration.ofSeconds(1))
.build();
}
@Bean
OAuth2AuthorizedClientManager authorizedClientManager() {
var authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
var authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, oAuth2AuthorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
Abfangjäger
public class OAuthClientCredentialsRestTemplateInterceptor implements ClientHttpRequestInterceptor {
private final OAuth2AuthorizedClientManager manager;
private final Authentication principal;
private final ClientRegistration clientRegistration;
public OAuthClientCredentialsRestTemplateInterceptor(OAuth2AuthorizedClientManager manager, ClientRegistration clientRegistration) {
this.manager = manager;
this.clientRegistration = clientRegistration;
this.principal = createPrincipal();
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
OAuth2AuthorizeRequest oAuth2AuthorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(clientRegistration.getRegistrationId())
.principal(principal)
.build();
OAuth2AuthorizedClient client = manager.authorize(oAuth2AuthorizeRequest);
if (isNull(client)) {
throw new IllegalStateException("client credentials flow on " + clientRegistration.getRegistrationId() + " failed, client is null");
}
request.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_PREFIX + client.getAccessToken().getTokenValue());
return execution.execute(request, body);
}
private Authentication createPrincipal() {
return new Authentication() {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.emptySet();
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getDetails() {
return null;
}
@Override
public Object getPrincipal() {
return this;
}
@Override
public boolean isAuthenticated() {
return false;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
@Override
public String getName() {
return clientRegistration.getClientId();
}
};
}
}
Dies generiert access_token beim ersten Aufruf und immer dann, wenn das Token abgelaufen ist. OAuth2AuthorizedClientManager verwaltet all dies für Sie
Ich mag diesen Ansatz, weil er mit dem von Spring Security übereinstimmt DefaultClientCredentialsTokenResponseClient was nutzt RestTemplate im Inneren. Das bedeutet, dass mein Projekt keine Abhängigkeit von spring-webflux benötigt.
– Nathan
21. Juli 2022 um 2:38 Uhr
beste Lösung. Prägnant und funktioniert wie ein Zauber. Selbst die Frühlingsdokumente boten keine so klare Anleitung.
– Walnussbär
17. August 2022 um 10:04 Uhr
Nur eine kleine Anmerkung, basierend auf dem Quellcode scheint es mir so AuthorizedClientServiceOAuth2AuthorizedClientManager ist nicht streng Thread-sicher. Nicht in einer Weise, die die Anwendung zum Absturz bringt, aber es werden mehrere Aufrufe zum Abrufen neuer Tokens getätigt, wenn mehrere Anforderungen gleichzeitig bearbeitet werden und alle einen Token verwenden, der bald abläuft/abgelaufen ist.
– Alexis
16. September 2022 um 7:25 Uhr
@Alexis wie können wir das vermeiden? Ich meine, die abgelaufenen nicht zu verwenden?
– John Roschan
22. Dezember 2022 um 16:48 Uhr
@JohnRoshan Es aktualisiert Token bei Bedarf automatisch, es ist nur so, dass es in einigen Grenzfällen mehrere Aktualisierungen parallel vornehmen kann, von denen einige redundant wären. Um dies zu vermeiden, müssten Sie die Synchronisierung selbst durchführen.
– Alexis
22. Dezember 2022 um 17:36 Uhr
Ich fand die Antwort von @matt Williams sehr hilfreich. Ich möchte jedoch hinzufügen, falls jemand clientId und Geheimnis für die WebClient-Konfiguration programmgesteuert übergeben möchte. Hier ist, wie es gemacht werden kann.
@Configuration
public class WebClientConfig {
public static final String TEST_REGISTRATION_ID = "test-client";
@Bean
public ReactiveClientRegistrationRepository clientRegistrationRepository() {
var clientRegistration = ClientRegistration.withRegistrationId(TEST_REGISTRATION_ID)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.clientId("<client_id>")
.clientSecret("<client_secret>")
.tokenUri("<token_uri>")
.build();
return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
}
@Bean
public WebClient testWebClient(ReactiveClientRegistrationRepository clientRegistrationRepo) {
var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepo, new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
oauth.setDefaultClientRegistrationId(TEST_REGISTRATION_ID);
return WebClient.builder()
.baseUrl("https://.test.com")
.filter(oauth)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
}
}
Gibt es einen Beispielcode, der für das obige Code-Snippet getestet werden kann?
– Sagar Pilkhwal
15. Juni 2020 um 14:41 Uhr
@SagarPilkhwal Sie können eine einfache, auf Federsicherheit basierende Beispiel-Spring-Boot-Anwendung erstellen (die Sie leicht online finden können). Legen Sie dort den client_credentials-basierten Zugriff fest und stellen Sie eine Test-API bereit. Dann können Sie WebClient mit dem obigen Code erstellen und versuchen, diese API aufzurufen.
– Jogger
15. Juni 2020 um 18:26 Uhr
Die obige Antwort von @Anar Sultanov hat mir geholfen, an diesen Punkt zu gelangen, aber da ich meiner OAuth-Token-Anforderung einige zusätzliche Header hinzufügen musste, dachte ich, ich würde eine vollständige Antwort darauf geben, wie ich das Problem für meinen Anwendungsfall gelöst habe.
Da dies eine Server-zu-Server-Kommunikation ist, müssen wir die verwenden ServerOAuth2AuthorizedClientExchangeFilterFunction. Dies akzeptiert nur a ReactiveOAuth2AuthorizedClientManagernicht die nicht reaktive OAuth2AuthorizedClientManager. Daher, wenn wir verwenden ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider() (um ihm den Anbieter zu geben, der für die OAuth2-Anfrage verwendet werden soll) müssen wir ihm a geben ReactiveOAuth2AuthorizedClientProvider anstelle der nicht reaktiven OAuth2AuthorizedClientProvider. Gemäß der spring-security Referenzdokumentation wenn Sie ein nicht reaktives verwenden DefaultClientCredentialsTokenResponseClient du kannst den … benutzen .setRequestEntityConverter() -Methode zum Ändern der OAuth2-Tokenanforderung, sondern das reaktive Äquivalent WebClientReactiveClientCredentialsTokenResponseClient bietet diese Möglichkeit nicht, also müssen wir unsere eigene implementieren (wir können die vorhandene verwenden WebClientReactiveClientCredentialsTokenResponseClient Logik).
Meine Implementierung wurde aufgerufen UaaWebClientReactiveClientCredentialsTokenResponseClient (Implementierung weggelassen, da sie die headers() Und body() Methoden aus der Vorgabe WebClientReactiveClientCredentialsTokenResponseClient um einige zusätzliche Header/Body-Felder hinzuzufügen, ändert dies nicht den zugrunde liegenden Authentifizierungsablauf).
Konfigurieren WebClient
Der ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient() Methode ist veraltet, also folgen Sie den Verwerfungshinweisen dieser Methode:
Veraltet. Verwenden ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager) stattdessen. Erstellen Sie eine Instanz von ClientCredentialsReactiveOAuth2AuthorizedClientProvider konfiguriert mit a WebClientReactiveClientCredentialsTokenResponseClient (oder ein benutzerdefiniertes) und dann liefern DefaultReactiveOAuth2AuthorizedClientManager.
Dies endet mit einer Konfiguration, die in etwa so aussieht:
@Bean("oAuth2WebClient")
public WebClient oauthFilteredWebClient(final ReactiveClientRegistrationRepository
clientRegistrationRepository)
{
final ClientCredentialsReactiveOAuth2AuthorizedClientProvider
clientCredentialsReactiveOAuth2AuthorizedClientProvider =
new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
clientCredentialsReactiveOAuth2AuthorizedClientProvider.setAccessTokenResponseClient(
new UaaWebClientReactiveClientCredentialsTokenResponseClient());
final DefaultReactiveOAuth2AuthorizedClientManager defaultReactiveOAuth2AuthorizedClientManager =
new DefaultReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository,
new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
defaultReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider(
clientCredentialsReactiveOAuth2AuthorizedClientProvider);
final ServerOAuth2AuthorizedClientExchangeFilterFunction oAuthFilter =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(defaultReactiveOAuth2AuthorizedClientManager);
oAuthFilter.setDefaultClientRegistrationId("uaa");
return WebClient.builder()
.filter(oAuthFilter)
.build();
}
Verwenden WebClient wie normal
Der oAuth2WebClient bean kann jetzt für den Zugriff auf Ressourcen verwendet werden, die durch unseren konfigurierten OAuth2-Anbieter geschützt sind, so wie Sie jede andere Anfrage mit a stellen würden WebClient.
Dies ist eine einfache Alternative zu OAuth2RestTemplate. Das folgende Snippet wurde mit Spring Boot getestet 3.0.0-M4 und es gibt keine application.yml Konfiguration erforderlich.
SecurityConfig.java
@Bean
public ReactiveClientRegistrationRepository getRegistration() {
ClientRegistration registration = ClientRegistration
.withRegistrationId("custom")
.tokenUri("<token_URI>")
.clientId("<client_id>")
.clientSecret("<secret>")
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build();
return new InMemoryReactiveClientRegistrationRepository(registration);
}
@Bean
public WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
InMemoryReactiveOAuth2AuthorizedClientService clientService = new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations);
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, clientService);
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth.setDefaultClientRegistrationId("custom");
return WebClient.builder()
.filter(oauth)
.filter(errorHandler()) // This is an optional
.build();
}
public static ExchangeFilterFunction errorHandler() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
if (clientResponse.statusCode().is5xxServerError() || clientResponse.statusCode().is4xxClientError()) {
return clientResponse.bodyToMono(String.class)
.flatMap(errorBody -> Mono.error(new IllegalAccessException(errorBody)));
} else {
return Mono.just(clientResponse);
}
});
}