Spring Security 5 Ersatz für OAuth2RestTemplate

14

In spring-security-oauth2:2.4.0.RELEASEKlassen wie OAuth2RestTemplate, OAuth2ProtectedResourceDetailsund ClientCredentialsAccessTokenProvideralle haben markiert als veraltet.

Aus dem Javadoc zu diesen Klassen geht hervor, dass ein Leitfaden zur Migration der Frühlingssicherheit darauf hinweist, dass Personen zum Kernprojekt der Frühlingssicherheit 5 migrieren sollten. Ich habe jedoch Probleme zu finden, wie ich meinen Anwendungsfall in diesem Projekt implementieren würde.

In der gesamten Dokumentation und in den Beispielen wird die Integration in einen OAuth-Anbieter eines Drittanbieters beschrieben, wenn eingehende Anforderungen an Ihre Anwendung authentifiziert werden sollen und Sie den OAuth-Anbieter eines Drittanbieters zur Überprüfung der Identität verwenden möchten.

In meinem Anwendungsfall möchte ich lediglich eine Anfrage RestTemplatean einen externen Dienst stellen, der durch OAuth geschützt ist. Derzeit erstelle ich eine OAuth2ProtectedResourceDetailsmit meiner Kunden-ID und meinem Geheimnis, die ich an eine weitergebe OAuth2RestTemplate. Ich habe auch eine benutzerdefinierte ClientCredentialsAccessTokenProviderhinzugefügt, die OAuth2ResTemplatenur einige zusätzliche Header zur Token-Anforderung hinzufügt, die von dem von mir verwendeten OAuth-Anbieter benötigt werden.

In der Spring-Security 5-Dokumentation habe ich einen Abschnitt gefunden, in dem das Anpassen der Token-Anforderung erwähnt wird , der jedoch wiederum im Zusammenhang mit der Authentifizierung einer eingehenden Anforderung bei einem OAuth-Anbieter eines Drittanbieters steht. Es ist nicht klar, wie Sie dies in Kombination mit so etwas wie a verwenden würden, ClientHttpRequestInterceptorum sicherzustellen, dass jede ausgehende Anforderung an einen externen Dienst zuerst ein Token erhält und dieses dann zur Anforderung hinzufügt.

Auch in dem oben verlinkten Migrationshandbuch wird auf ein Verweis verwiesen, von OAuth2AuthorizedClientServicedem es sagt, dass es für die Verwendung in Interceptors nützlich ist, aber auch dies ClientRegistrationRepositoryscheint auf Dingen wie dem zu beruhen , in denen Registrierungen für Drittanbieter verwaltet werden, wenn Sie diese verwenden möchten Diese sorgen dafür, 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 ausgehenden Anforderungen aus meiner Anwendung hinzugefügt werden kann?

Matt Williams
quelle

Antworten:

15

OAuth 2.0-Clientfunktionen von Spring Security 5.2.x werden nicht unterstützt RestTemplate, sondern nur WebClient. Siehe Spring Security Reference :

HTTP-Client-Unterstützung

  • WebClient Integration für Servlet-Umgebungen (zum Anfordern geschützter Ressourcen)

Darüber hinaus RestTemplatewird in einer zukünftigen Version veraltet sein. Siehe RestTemplate javadoc :

HINWEIS: Ab 5.0 bietet das nicht blockierende Reaktiv org.springframework.web.reactive.client.WebClienteine moderne Alternative zu dem RestTemplatemit effizienter Unterstützung für Synchronisierung und Asynchronisierung sowie Streaming-Szenarien. Das RestTemplatewird in einer zukünftigen Version veraltet sein und es werden in Zukunft keine größeren neuen Funktionen hinzugefügt. WebClientWeitere Informationen und Beispielcode finden Sie im Abschnitt der Spring Framework-Referenzdokumentation.

Daher wäre die beste Lösung, RestTemplatezugunsten von aufzugeben WebClient.


Verwenden WebClientfür den Clientanmeldeinformationsfluss

Konfigurieren Sie die Client-Registrierung und den Anbieter entweder programmgesteuert oder mithilfe der automatischen Spring Boot-Konfiguration:

spring:
  security:
    oauth2:
      client:
        registration:
          custom:
            client-id: clientId
            client-secret: clientSecret
            authorization-grant-type: client_credentials
        provider:
          custom:
            token-uri: http://localhost:8081/oauth/token

… Und die OAuth2AuthorizedClientManager @Bean:

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .clientCredentials()
                    .build();

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

Konfigurieren Sie die WebClientzu verwendende Instanz ServerOAuth2AuthorizedClientExchangeFilterFunctionmit den folgenden Funktionen OAuth2AuthorizedClientManager:

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth2Client.setDefaultClientRegistrationId("custom");
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build();
}

Wenn Sie nun versuchen, mithilfe dieser WebClientInstanz eine Anforderung zu stellen, fordert diese zuerst ein Token vom Autorisierungsserver an und nimmt es in die Anforderung auf.

Anar Sultanov
quelle
Vielen Dank, das klärt ein paar Dinge auf, aber aus der oben verlinkten Dokumentation habe ich immer noch Schwierigkeiten, ein Beispiel zu finden, in dem ein Interceptor (oder was auch immer die neue Terminologie WebClientist) oder ähnliches verwendet wird, um ein OAuth-Token von a abzurufen Benutzerdefinierter OAuth-Anbieter (keiner der OoTB-unterstützten Anbieter wie Facebook / Google), um ihn einer ausgehenden Anfrage hinzuzufügen. Alle Beispiele scheinen sich auf die Authentifizierung eingehender Anfragen bei anderen Anbietern zu konzentrieren. Haben Sie Hinweise für gute Beispiele?
Matt Williams
1
@MattWilliams Ich habe die Antwort mit einem Beispiel für die Verwendung WebClientmit dem Grant-Typ für Client-Anmeldeinformationen aktualisiert .
Anar Sultanov
Perfekt, das alles macht jetzt viel mehr Sinn, vielen Dank. Ich werde vielleicht ein paar Tage lang keine Gelegenheit haben, es auszuprobieren, aber ich werde sicher wiederkommen und dies als richtige Antwort markieren, sobald ich es versucht habe
Matt Williams
1
Das ist jetzt auch veraltet lol ... zumindest UnAuthenticatedServerOAuth2AuthorizedClientRepository ist ...
SledgeHammer
Danke @SledgeHammer, ich habe meine Antwort aktualisiert.
Anar Sultanov
1

Die obige Antwort von @Anar Sultanov hat mir geholfen, an diesen Punkt zu gelangen, aber da ich meiner OAuth-Token-Anfrage 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.

Konfigurieren Sie die Providerdetails

Fügen Sie Folgendes hinzu: application.properties

spring.security.oauth2.client.registration.uaa.client-id=${CLIENT_ID:}
spring.security.oauth2.client.registration.uaa.client-secret=${CLIENT_SECRET:}
spring.security.oauth2.client.registration.uaa.scope=${SCOPE:}
spring.security.oauth2.client.registration.uaa.authorization-grant-type=client_credentials
spring.security.oauth2.client.provider.uaa.token-uri=${UAA_URL:}

Implementieren Sie benutzerdefinierte ReactiveOAuth2AccessTokenResponseClient

Da dies eine Server-zu-Server-Kommunikation ist, müssen wir die verwenden ServerOAuth2AuthorizedClientExchangeFilterFunction. Dies akzeptiert nur a ReactiveOAuth2AuthorizedClientManager, nicht das nicht reaktive OAuth2AuthorizedClientManager. Daher müssen wir bei der Verwendung ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider()(um dem Anbieter die OAuth2-Anforderung zu geben) eine ReactiveOAuth2AuthorizedClientProvideranstelle der nicht reaktiven geben OAuth2AuthorizedClientProvider. Gemäß der Spring-Security-ReferenzdokumentationDefaultClientCredentialsTokenResponseClient können Sie die .setRequestEntityConverter()Methode zum Ändern der OAuth2-Token-Anforderung verwenden, wenn Sie eine nicht reaktive verwenden. Das reaktive Äquivalent WebClientReactiveClientCredentialsTokenResponseClientbietet diese Funktion jedoch nicht. Daher müssen wir unsere eigene implementieren (wir können sie verwenden) die bestehende WebClientReactiveClientCredentialsTokenResponseClientLogik).

Meine Implementierung wurde aufgerufen UaaWebClientReactiveClientCredentialsTokenResponseClient(die Implementierung wurde weggelassen, da sie die Methoden headers()und nur geringfügig body()von der Standardeinstellung WebClientReactiveClientCredentialsTokenResponseClientzum Hinzufügen einiger zusätzlicher Header / Body-Felder ändert und den zugrunde liegenden Authentifizierungsfluss nicht ändert).

Konfigurieren WebClient

Die ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient()Methode wurde veraltet. Befolgen Sie daher die Empfehlungen zur Abschreibung dieser Methode:

Veraltet. Verwenden Sie ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager)stattdessen. Erstellen Sie eine ClientCredentialsReactiveOAuth2AuthorizedClientProvidermit einer WebClientReactiveClientCredentialsTokenResponseClient(oder einer benutzerdefinierten) konfigurierte Instanz und geben Sie sie dann an DefaultReactiveOAuth2AuthorizedClientManager.

Dies führt dazu, dass die Konfiguration ungefähr 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();
}

Verwendung WebClientals normal

Die oAuth2WebClientBean kann jetzt verwendet werden, um auf Ressourcen zuzugreifen, die von unserem konfigurierten OAuth2-Anbieter geschützt werden, so wie Sie jede andere Anforderung mit a stellen würden WebClient.

Matt Williams
quelle
Wie übergebe ich programmgesteuert eine Client-ID, ein Client-Geheimnis und einen Oauth-Endpunkt?
Montag,
Ich habe dies nicht versucht, aber es sieht so aus, als könnten Sie Instanzen von ClientRegistrations mit den erforderlichen Details erstellen und diese an den Konstruktor für InMemoryReactiveClientRegistrationRepository(die Standardimplementierung von ReactiveClientRegistrationRepository) übergeben. Sie verwenden dann diese neu erstellte InMemoryReactiveClientRegistrationRepositoryBohne anstelle meiner automatisch verdrahteten Bohne clientRegistrationRepository, die an die oauthFilteredWebClientMethode übergeben wird
Matt Williams
Mh, aber ich kann mich zur ClientRegistrationLaufzeit nicht anders registrieren , oder? Soweit ich verstanden habe, muss ich ClientRegistrationbeim Start eine Bean erstellen .
Montag,
Ah ok, ich dachte du wolltest sie nur nicht in der application.propertiesDatei deklarieren . Ihre eigene Implementierung von ReactiveOAuth2AccessTokenResponseClientermöglicht es Ihnen , zu machen , was fordern Sie die OAuth2 Token zu bekommen, aber ich weiß nicht , wie Sie einen dynamischen „Kontext‘ , um es per Anfrage zur Verfügung stellen könnten. Das gleiche gilt für geht , wenn Sie Ihre eigenen gesamten Filter implementiert. Das alles Wenn Sie nicht ableiten könnten, was Sie von dort benötigen, bin ich mir nicht sicher, welche Optionen Sie haben. Was ist Ihr Anwendungsfall? Warum sollten Sie die möglichen Registrierungen beim Start nicht kennen?
Matt Williams
1

Ich fand @matt Williams Antwort ziemlich hilfreich. Ich möchte jedoch hinzufügen, falls jemand die Client-ID und das Geheimnis für die WebClient-Konfiguration programmgesteuert übergeben möchte. So kann es gemacht werden.

 @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);
    }
}
Jogger
quelle
0

Hallo, vielleicht ist es zu spät, aber RestTemplate wird in Spring Security 5 weiterhin unterstützt. Für nicht reaktive Apps wird RestTemplate weiterhin verwendet. Sie müssen lediglich die Federsicherheit ordnungsgemäß konfigurieren und einen Interceptor erstellen, wie im Migrationshandbuch angegeben

Verwenden Sie die folgende Konfiguration, um den Fluss client_credentials zu verwenden

application.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: ${okta.oauth2.issuer}/v1/keys
      client:
        registration:
          okta:
            client-id: ${okta.oauth2.clientId}
            client-secret: ${okta.oauth2.clientSecret}
            scope: "custom-scope"
            authorization-grant-type: client_credentials
            provider: okta
        provider:
          okta:
            authorization-uri: ${okta.oauth2.issuer}/v1/authorize
            token-uri: ${okta.oauth2.issuer}/v1/token

Konfiguration zu OauthResTemplate

@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

Leandro Assis
quelle