Замена Spring Security 5 для OAuth2RestTemplate


14

В spring-security-oauth2:2.4.0.RELEASEклассах , таких как OAuth2RestTemplate, OAuth2ProtectedResourceDetailsи ClientCredentialsAccessTokenProviderвсе были помечены как нежелательные.

От javadoc для этих классов это указывает на руководство по миграции безопасности Spring, которое подсказывает, что люди должны перейти к основному проекту Spring-Security 5. Однако у меня возникают проблемы с поиском того, как бы я реализовал свой вариант использования в этом проекте.

Во всей документации и примерах рассказывается об интеграции с 3-сторонним поставщиком OAuth, если вы хотите, чтобы входящие запросы к вашему приложению были аутентифицированы, и вы хотите использовать сторонний OAuth-поставщик для проверки личности.

В моем случае использования все, что я хочу сделать, это сделать запрос RestTemplateк внешнему сервису, защищенному OAuth. В настоящее время я создаю OAuth2ProtectedResourceDetailsсвой идентификатор клиента и секрет, который я передаю в OAuth2RestTemplate. Я также обычай ClientCredentialsAccessTokenProviderдобавил в OAuth2ResTemplateтом , что только добавляет некоторые дополнительные заголовки в маркер запроса, которые требуются от поставщика OAuth я использую.

В документации Spring-Security 5 я нашел раздел, в котором упоминается настройка запроса токена , но опять-таки он выглядит в контексте аутентификации входящего запроса у стороннего поставщика OAuth. Непонятно, как бы вы использовали это в сочетании с чем-то вроде a, ClientHttpRequestInterceptorчтобы каждый исходящий запрос к внешней службе сначала получал токен, а затем добавлял его в запрос.

Также в приведенном выше руководстве по миграции есть ссылка на то, OAuth2AuthorizedClientServiceчто, по его словам, полезно использовать в перехватчиках, но опять-таки это похоже на то, что оно опирается на такие вещи, ClientRegistrationRepositoryкоторые, по-видимому, и поддерживают регистрацию сторонних поставщиков, если вы хотите использовать которые обеспечивают проверку входящего запроса.

Можно ли каким-то образом использовать новые функции Spring-Security 5 для регистрации поставщиков OAuth, чтобы получить токен для добавления к исходящим запросам из моего приложения?

Ответы:


15

Функции клиента OAuth 2.0 в Spring Security 5.2.x не поддерживаются RestTemplate, а только поддерживаются WebClient. См. Spring Security Reference :

Поддержка HTTP-клиента

  • WebClient интеграция для сред сервлетов (для запроса защищенных ресурсов)

Кроме того, RestTemplateбудет объявлен устаревшим в следующей версии. Смотрите RestTemplate Javadoc :

ПРИМЕЧАНИЕ. Начиная с версии 5.0 неблокирующая реактивная система org.springframework.web.reactive.client.WebClientпредлагает современную альтернативу RestTemplateс эффективной поддержкой синхронизации и асинхронности, а также сценариев потоковой передачи. В RestTemplateбудущей версии он будет устаревшим, и в будущем не будут добавлены новые важные функции. См. WebClientРаздел справочной документации Spring Framework для получения дополнительной информации и примера кода.

Поэтому лучшим решением было бы отказаться RestTemplateв пользу WebClient.


Использование WebClientдля потока учетных данных клиента

Сконфигурируйте клиентскую регистрацию и провайдера либо программно, либо с помощью автоконфигурации 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

... и 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;
}

Настройте WebClientэкземпляр для использования ServerOAuth2AuthorizedClientExchangeFilterFunctionс предоставленным OAuth2AuthorizedClientManager:

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

Теперь, если вы попытаетесь сделать запрос с использованием этого WebClientэкземпляра, он сначала запросит токен с сервера авторизации и включит его в запрос.


Спасибо, это проясняет некоторые вещи, но из всей приведенной выше документации я все еще пытаюсь найти пример, где перехватчик (или какова бы ни была новая терминология WebClient) или что-то подобное используется для получения токена OAuth из пользовательский поставщик OAuth (не один из поддерживаемых OoTB, как Facebook / Google), чтобы добавить его в исходящий запрос. Все примеры, кажется, сосредоточены на аутентификации входящих запросов с другими провайдерами. Есть ли у вас какие-либо указатели для каких-либо хороших примеров?
Мэтт Уильямс

1
@MattWilliams Я обновил ответ с примером того, как использовать WebClientс типом предоставления учетных данных клиента.
Анар Султанов

Прекрасно, теперь все это имеет больше смысла, большое спасибо. Возможно, у меня не будет возможности попробовать его в течение нескольких дней, но я обязательно вернусь и отмечу это как правильный ответ, как только у меня
Мэтт Уильямс,

1
Это уже не рекомендуется, смеется ... по крайней мере UnAuthenticatedServerOAuth2AuthorizedClientRepository ...
SledgeHammer

Спасибо @SledgeHammer, я обновил свой ответ.
Анар Султанов

1

Приведенный выше ответ @Anar Sultanov помог мне добраться до этой точки, но, поскольку мне пришлось добавить несколько дополнительных заголовков к моему запросу токена OAuth, я подумал, что предоставлю полный ответ о том, как я решил проблему для своего варианта использования.

Настроить детали провайдера

Добавьте следующее к 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:}

Реализуйте пользовательские ReactiveOAuth2AccessTokenResponseClient

Поскольку это межсерверная связь, нам нужно использовать ServerOAuth2AuthorizedClientExchangeFilterFunction. Это только принимает ReactiveOAuth2AuthorizedClientManager, а не реагирует OAuth2AuthorizedClientManager. Поэтому, когда мы используем ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider()(чтобы поставщик использовал его для выполнения запроса OAuth2), мы должны предоставить его ReactiveOAuth2AuthorizedClientProviderвместо нереактивного OAuth2AuthorizedClientProvider. Согласно справочной документации по безопасности пружин, если вы используете нереактивный, DefaultClientCredentialsTokenResponseClientвы можете использовать .setRequestEntityConverter()метод для изменения запроса токена OAuth2, но реактивный эквивалент WebClientReactiveClientCredentialsTokenResponseClientне предоставляет эту возможность, поэтому мы должны реализовать свое собственное (мы можем использовать существующая WebClientReactiveClientCredentialsTokenResponseClientлогика).

Моя реализация была вызвана UaaWebClientReactiveClientCredentialsTokenResponseClient(реализация опущена, поскольку она лишь очень незначительно изменяет методы headers()и body()по умолчанию WebClientReactiveClientCredentialsTokenResponseClientдля добавления некоторых дополнительных полей заголовков / тела, она не меняет основной поток аутентификации).

Настройка WebClient

ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient()Метод устарел, поэтому следуя совету устаревания от этого метода:

Устаревшее. Используйте ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager)вместо этого. Создайте экземпляр, ClientCredentialsReactiveOAuth2AuthorizedClientProviderнастроенный с WebClientReactiveClientCredentialsTokenResponseClient(или пользовательский), и затем предоставьте его DefaultReactiveOAuth2AuthorizedClientManager.

В итоге конфигурация выглядит примерно так:

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

Используйте WebClientкак обычно

Теперь oAuth2WebClientbean-компонент готов к использованию для доступа к ресурсам, защищенным нашим сконфигурированным поставщиком OAuth2, таким же образом, как если бы вы выполняли любой другой запрос, используя a WebClient.


Как программно передать идентификатор клиента, секрет клиента и конечную точку oauth?
Монти

Я не пробовал этого, но похоже, что вы можете создать экземпляры ClientRegistrations с необходимыми деталями и передать их в конструктор InMemoryReactiveClientRegistrationRepository(реализация по умолчанию ReactiveClientRegistrationRepository). Затем вы используете этот недавно созданный InMemoryReactiveClientRegistrationRepositoryбоб вместо моего автопровода, clientRegistrationRepositoryкоторый передается в oauthFilteredWebClientметод
Мэтт Уильямс

Мм, но я не могу зарегистрировать другое ClientRegistrationво время выполнения, не так ли? Насколько я понял, мне нужно создать компонент ClientRegistrationпри запуске.
Монти

Ах, хорошо, я думал, вы просто не хотите объявлять их в application.propertiesфайле. Реализация собственного ReactiveOAuth2AccessTokenResponseClientпозволяет вам сделать любой запрос, который вы хотите получить токен OAuth2, но я не знаю, как вы могли бы предоставить ему динамический «контекст» для каждого запроса. То же самое можно сказать, если вы реализовали свой собственный полный фильтр. Все это дал бы вам доступ к исходящему запросу, поэтому, если вы не сможете определить, что вам нужно оттуда, я не уверен, какие у вас есть варианты. Каков ваш вариант использования? Почему вы не знаете возможные регистрации при запуске?
Мэтт Уильямс

1

Я нашел ответ @matt Williams весьма полезным. Хотя я хотел бы добавить, если кто-то хотел бы программно передать clientId и секрет для конфигурации WebClient. Вот как это можно сделать.

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

0

Привет, может быть, уже слишком поздно, но RestTemplate все еще поддерживается в Spring Security 5, чтобы нереактивное приложение все еще используется RestTemplate, вам нужно только правильно настроить Spring Security и создать перехватчик, как указано в руководстве по миграции.

Используйте следующую конфигурацию для использования потока client_credentials

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

Конфигурация для 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;
    }

}

истребитель-перехватчик

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

Это сгенерирует access_token при первом вызове и всякий раз, когда токен истекает. OAuth2AuthorizedClientManager будет управлять всем этим вам

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.