Весна безпека 5 Заміна для OAuth2RestTemplate


14

У spring-security-oauth2:2.4.0.RELEASEтаких класах, як OAuth2RestTemplate, OAuth2ProtectedResourceDetailsі ClientCredentialsAccessTokenProviderвсі вони були позначені як застарілі.

З цього питання про javadoc вказує на весняний посібник з міграції безпеки, який наголошує на тому, що люди повинні перейти до основного проекту Spring-security 5. Однак у мене виникають проблеми з пошуком того, як я реалізував би свій випадок використання в цьому проекті.

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

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

У документації до джерела безпеки 5 я знайшов розділ, в якому йдеться про налаштування запиту токена , але знову це виглядає в контексті аутентифікації вхідного запиту з стороннім постачальником OAuth. Незрозуміло, як би ви використовували це в поєднанні з чимось на кшталт a, ClientHttpRequestInterceptorщоб забезпечити, щоб кожен вихідний запит на зовнішню службу спочатку отримував маркер, а потім отримує доданий до запиту.

Також у посібнику з міграції, зв'язаному вище, є посилання на те, OAuth2AuthorizedClientServiceяке, на його думку, є корисним для використання в перехоплювачах, але, знову ж таки, це виглядає так, що він покладається на такі речі, як, ClientRegistrationRepositoryздається, там, де він підтримує реєстрацію для сторонніх постачальників, якщо ви хочете використовувати які забезпечують аутентифікацію вхідного запиту.

Чи я можу використати нову функціональність у Spring-security 5 для реєстрації провайдерів OAuth, щоб отримати маркер для додавання до вихідних запитів моєї програми?

Відповіді:


15

Особливості клієнта OAuth 2.0 Spring Security 5.2.x не підтримують RestTemplate, але лише WebClient. Дивіться довідкові джерела безпеки :

Підтримка HTTP-клієнта

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

Крім того, RestTemplateу наступній версії буде застаріло. Дивіться RevaTemplate 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
Це застаріле зараз занадто лол ... принаймні UnAuthenticationServerOAuth2AuthorizedClientRepository is ...
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як звичайне

Тепер oAuth2WebClientбоб готовий використовуватись для доступу до ресурсів, захищених нашим налаштованим постачальником OAuth2 так, як ви зробили б будь-який інший запит, використовуючи WebClient.


Як програмно передавати ідентифікатор клієнта, секрет клієнта та кінцеву точку oauth?
monti

Я цього не пробував, але, схоже, ви можете створити екземпляри ClientRegistrations з необхідними деталями і передати їх у конструктор для InMemoryReactiveClientRegistrationRepository(реалізація за замовчуванням ReactiveClientRegistrationRepository). Потім ви використовуєте щойно створений InMemoryReactiveClientRegistrationRepositoryбоб замість мого автопроводу, clientRegistrationRepositoryякий передається oauthFilteredWebClientметоду
Метт Вільямс

Mh, але я не можу зареєструвати різні 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 все ще використовується, що вам потрібно зробити, це лише правильно налаштувати захист весни та створити перехоплювач, як згадується в посібнику з міграції

Для використання потоку 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.