Spring Security 5 Zamiennik dla OAuth2RestTemplate

14

W spring-security-oauth2:2.4.0.RELEASEklasach, takich jak OAuth2RestTemplate, OAuth2ProtectedResourceDetailsi ClientCredentialsAccessTokenProviderwszystkie zostały oznaczone jako przestarzałe.

Z javadoc na tych klasach wskazuje przewodnik po wiosennej migracji bezpieczeństwa, który sugeruje, że ludzie powinni migrować do podstawowego projektu wiosennego bezpieczeństwa 5. Mam jednak problem ze znalezieniem sposobu, w jaki mógłbym zaimplementować mój przypadek użycia w tym projekcie.

Cała dokumentacja i przykłady mówią o integracji z zewnętrznym dostawcą OAuth, jeśli chcesz, aby przychodzące żądania do Twojej aplikacji były uwierzytelniane i chcesz użyć zewnętrznego dostawcy OAuth do weryfikacji tożsamości.

W moim przypadku użytkowania chcę jedynie wysłać zapytanie RestTemplatedo usługi zewnętrznej chronionej przez OAuth. Obecnie tworzę OAuth2ProtectedResourceDetailsidentyfikator klienta i klucz tajny, które przekazuję OAuth2RestTemplate. Mam również niestandardowy ClientCredentialsAccessTokenProviderdodany do, OAuth2ResTemplatektóry po prostu dodaje dodatkowe nagłówki do żądania tokenu, które są wymagane przez dostawcę OAuth, którego używam.

W dokumentacji Spring-security 5 znalazłem sekcję, która wspomina o dostosowywaniu żądania tokenu , ale znowu wygląda na to, że jest w kontekście uwierzytelniania przychodzącego żądania z zewnętrznym dostawcą OAuth. Nie jest jasne, w jaki sposób można tego użyć w połączeniu z czymś w rodzaju a, ClientHttpRequestInterceptoraby upewnić się, że każde wychodzące żądanie do usługi zewnętrznej najpierw pobiera token, a następnie dodaje je do żądania.

Również w powyższym przewodniku migracji znajduje się odniesienie do, OAuth2AuthorizedClientServicektóre według niego jest przydatne do użycia w przechwytywaczach, ale znowu wygląda na to, że opiera się na takich rzeczach, jak się ClientRegistrationRepositorywydaje, gdy zachowuje rejestracje dla dostawców zewnętrznych, jeśli chcesz użyć które zapewniają, że przychodzące żądanie jest uwierzytelniane.

Czy jest jakiś sposób, aby wykorzystać nową funkcjonalność Spring-security 5 do rejestracji dostawców OAuth w celu uzyskania tokena do dodania do wychodzących żądań z mojej aplikacji?

Matt Williams
źródło

Odpowiedzi:

15

Funkcje klienta OAuth 2.0 Spring Security 5.2.x nie obsługują RestTemplate, ale tylko WebClient. Zobacz informacje na temat zabezpieczeń Spring :

Obsługa klienta HTTP

  • WebClient integracja środowisk serwletów (do żądania chronionych zasobów)

Ponadto RestTemplatezostanie wycofany w przyszłej wersji. Zobacz RestTemplate javadoc :

UWAGA: Od wersji 5.0 nieblokująca funkcja reaktywna org.springframework.web.reactive.client.WebClientoferuje nowoczesną alternatywę dla RestTemplatez efektywną obsługą synchronizacji i asynchronizacji, a także scenariuszy przesyłania strumieniowego. RestTemplateZostanie wycofany w przyszłej wersji i nie będzie mieć poważne nowe funkcje dodane w przyszłości. Aby WebClientuzyskać więcej informacji i przykładowy kod, zobacz sekcję dokumentacji źródłowej Spring Framework.

Dlatego najlepszym rozwiązaniem byłoby porzucić RestTemplatena korzyść WebClient.


Korzystanie WebClientz przepływu poświadczeń klienta

Skonfiguruj rejestrację klienta i dostawcę programowo lub przy użyciu automatycznej konfiguracji Spring Boot:

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

… I 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;
}

Skonfiguruj WebClientinstancję do używania ServerOAuth2AuthorizedClientExchangeFilterFunctionz dostarczonymi OAuth2AuthorizedClientManager:

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

Teraz, jeśli spróbujesz złożyć żądanie za pomocą tego WebClientwystąpienia, najpierw poprosi o token z serwera autoryzacji i umieści go w żądaniu.

Anar Sułtanow
źródło
Dzięki, że to wyjaśnia kilka rzeczy, ale z całej powyższej połączonej dokumentacji wciąż staram się znaleźć przykład, w którym przechwytywacz (lub cokolwiek to jest nowa terminologia WebClient) lub coś podobnego jest używane do pobrania tokena OAuth z niestandardowy dostawca OAuth (nie jeden z obsługiwanych OoTB, takich jak Facebook / Google) w celu dodania go do żądania wychodzącego. Wszystkie przykłady wydają się koncentrować na uwierzytelnianiu przychodzących wniosków z innymi dostawcami. Czy masz jakieś wskazówki dla dobrych przykładów?
Matt Williams
1
@MattWilliams Zaktualizowałem odpowiedź, podając przykład użycia WebClientz typem przyznania referencji klienta.
Anar Sułtanow
Idealnie, że teraz wszystko ma sens, dziękuję bardzo. Może nie będę miał szansy wypróbować go przez kilka dni, ale na pewno wrócę i oznaczę to jako poprawną odpowiedź, gdy będę miał okazję
Matt Williams
1
To jest przestarzałe teraz zbyt lol ... przynajmniej UnAuthenticatedServerOAuth2AuthorizedClientRepository jest ...
SledgeHammer
Dzięki @SledgeHammer, zaktualizowałem swoją odpowiedź.
Anar Sułtanow
1

Powyższa odpowiedź od @Anar Sultanov pomogła mi dojść do tego punktu, ale ponieważ musiałem dodać kilka dodatkowych nagłówków do mojej prośby o token OAuth, pomyślałem, że udzielę pełnej odpowiedzi na pytanie, jak rozwiązałem problem w moim przypadku użycia.

Skonfiguruj dane dostawcy

Dodaj następujące elementy do 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:}

Wdróż niestandardowe ReactiveOAuth2AccessTokenResponseClient

Ponieważ jest to komunikacja między serwerami, musimy skorzystać z ServerOAuth2AuthorizedClientExchangeFilterFunction. To akceptuje tylko ReactiveOAuth2AuthorizedClientManagerniereaktywne OAuth2AuthorizedClientManager. Dlatego kiedy używamy ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider()(aby dać temu dostawcy, aby używał do żądania OAuth2), musimy dać mu ReactiveOAuth2AuthorizedClientProviderzamiast nieaktywnego OAuth2AuthorizedClientProvider. Zgodnie z dokumentacją referencyjną zabezpieczenia sprężynowego, jeśli używasz niereaktywnego DefaultClientCredentialsTokenResponseClient, możesz użyć .setRequestEntityConverter()metody zmiany żądania tokena OAuth2, ale reaktywny odpowiednik WebClientReactiveClientCredentialsTokenResponseClientnie zapewnia tej funkcji, więc musimy wdrożyć własne (możemy skorzystać z istniejąca WebClientReactiveClientCredentialsTokenResponseClientlogika).

Moja implementacja została wywołana UaaWebClientReactiveClientCredentialsTokenResponseClient(implementacja została pominięta, ponieważ tylko nieznacznie zmienia metody headers()i body()od wartości domyślnych w WebClientReactiveClientCredentialsTokenResponseClientcelu dodania dodatkowych nagłówków / pól treści, nie zmienia podstawowego przepływu uwierzytelniania).

Konfiguruj WebClient

ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient()Metoda została zaniechana, więc za radą Wycofanie z tej metody:

Przestarzałe. Użyj ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager)zamiast tego. Utwórz instancję ClientCredentialsReactiveOAuth2AuthorizedClientProviderskonfigurowaną za pomocą WebClientReactiveClientCredentialsTokenResponseClient(lub niestandardowej) i dostarcz ją do DefaultReactiveOAuth2AuthorizedClientManager.

W rezultacie konfiguracja wygląda mniej więcej tak:

@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();
}

Używaj WebClientjak zwykle

oAuth2WebClientBean jest teraz gotowa do użycia dostępu do zasobów chronionych przez naszego dostawcę OAuth2 skonfigurowane w taki sposób, by można zrobić każdy inny wniosek użyciem WebClient.

Matt Williams
źródło
Jak przekazać programowo identyfikator klienta, klucz klienta i ostry punkt końcowy?
monti
Nie próbowałem tego, ale wygląda na to, że możesz utworzyć instancje ClientRegistrationz wymaganymi szczegółami i przekazać je do konstruktora dla InMemoryReactiveClientRegistrationRepository(domyślna implementacja ReactiveClientRegistrationRepository). Następnie używasz tej nowo utworzonej InMemoryReactiveClientRegistrationRepositoryfasoli zamiast mojego autowiredowanego, clientRegistrationRepositoryktóry jest przekazywany do oauthFilteredWebClientmetody
Matt Williams
Ale nie jestem w stanie zarejestrować się inaczej ClientRegistrationw czasie wykonywania, prawda? O ile rozumiem, muszę stworzyć fasolę ClientRegistrationprzy starcie.
monti,
Ach, ok, myślałem, że po prostu nie chcesz zadeklarować ich w application.propertiespliku. Wdrożenie własnego ReactiveOAuth2AccessTokenResponseClientpozwala na wykonanie dowolnego żądania, które chcesz uzyskać token OAuth2, ale nie wiem, w jaki sposób można zapewnić dynamiczny „kontekst” dla każdego żądania. To samo dotyczy, jeśli zaimplementowałeś własny filtr. dałbym ci dostęp do wychodzącej prośby, więc jeśli nie możesz wywnioskować z tego, czego potrzebujesz, nie jestem pewien, jakie masz opcje. Jaki jest twój przypadek użycia? Dlaczego nie znasz możliwych rejestracji podczas uruchamiania?
Matt Williams
1

Odpowiedź @matta Williamsa była bardzo pomocna. Chociaż chciałbym dodać na wypadek, gdyby ktoś chciał programowo przekazać clientId i sekret dla konfiguracji WebClient. Oto jak można to zrobić.

 @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);
    }
}
Uprawiający jogging
źródło
0

Cześć, może jest już za późno, jednak RestTemplate jest nadal obsługiwany w Spring Security 5, do niereaktywnej aplikacji RestTemplate jest nadal używany, musisz tylko odpowiednio skonfigurować zabezpieczenia Spring i utworzyć przechwytywacz, jak wspomniano w przewodniku migracji

Użyj następującej konfiguracji, aby użyć przepływu poświadczeń_klienta

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

Konfiguracja do 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;
    }

}

Przechwytywacz

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();
            }
        };
    }
}

To wygeneruje access_token w pierwszym wywołaniu i za każdym razem, gdy token straci ważność. OAuth2AuthorizedClientManager zarządza tym wszystkim

Leandro Assis
źródło