Bảo mật mùa xuân 5 Thay thế cho OAuth2RestTemplate


14

Trong spring-security-oauth2:2.4.0.RELEASEcác lớp như OAuth2RestTemplate, OAuth2ProtectedResourceDetailsClientCredentialsAccessTokenProvidertất cả đã được đánh dấu là không dùng nữa.

Từ javadoc trên các lớp này, nó chỉ ra một hướng dẫn di chuyển bảo mật mùa xuân , trong đó nhấn mạnh rằng mọi người nên di chuyển đến dự án 5 bảo mật mùa xuân cốt lõi. Tuy nhiên, tôi gặp khó khăn khi tìm cách tôi sẽ triển khai trường hợp sử dụng của mình trong dự án này.

Tất cả các tài liệu và ví dụ nói về việc tích hợp với nhà cung cấp OAuth phần 3 nếu bạn muốn các yêu cầu đến ứng dụng của bạn được xác thực và bạn muốn sử dụng nhà cung cấp OAuth của bên thứ 3 để xác minh danh tính.

Trong trường hợp sử dụng của tôi, tất cả những gì tôi muốn làm là đưa ra yêu cầu với một RestTemplatedịch vụ bên ngoài được bảo vệ bởi OAuth. Hiện tại tôi tạo một OAuth2ProtectedResourceDetailsid khách hàng và bí mật mà tôi chuyển vào OAuth2RestTemplate. Tôi cũng có một tùy chỉnh ClientCredentialsAccessTokenProviderđược thêm vào OAuth2ResTemplatemà chỉ cần thêm một số tiêu đề bổ sung vào yêu cầu mã thông báo được yêu cầu bởi nhà cung cấp OAuth mà tôi đang sử dụng.

Trong tài liệu bảo mật mùa xuân 5 tôi đã tìm thấy một phần đề cập đến việc tùy chỉnh yêu cầu mã thông báo , nhưng một lần nữa có vẻ như trong bối cảnh xác thực yêu cầu đến với nhà cung cấp OAuth bên thứ 3. Không rõ bạn sẽ sử dụng kết hợp này như thế nào ClientHttpRequestInterceptorđể đảm bảo rằng mỗi yêu cầu gửi đến một dịch vụ bên ngoài trước tiên sẽ nhận được mã thông báo và sau đó được thêm vào yêu cầu.

Ngoài ra, trong hướng dẫn di chuyển được liên kết ở trên có tham chiếu đến hướng dẫn OAuth2AuthorizedClientServicesử dụng trong phần mềm đánh chặn, nhưng một lần nữa, nó trông giống như những thứ giống như ClientRegistrationRepositorynơi nó duy trì đăng ký cho nhà cung cấp bên thứ ba nếu bạn muốn sử dụng cung cấp để đảm bảo yêu cầu đến được xác thực.

Có cách nào tôi có thể sử dụng chức năng mới trong bảo mật mùa xuân 5 để đăng ký nhà cung cấp OAuth để nhận mã thông báo để thêm vào các yêu cầu gửi đi từ ứng dụng của mình không?

Câu trả lời:


15

Các tính năng OAuth 2.0 Client của Spring Security 5.2.x không hỗ trợ RestTemplatemà chỉ hỗ trợ WebClient. Xem tài liệu tham khảo bảo mật mùa xuân :

Hỗ trợ khách hàng HTTP

  • WebClient tích hợp cho Môi trường Servlet (để yêu cầu tài nguyên được bảo vệ)

Ngoài ra, RestTemplatesẽ không được chấp nhận trong phiên bản tương lai. Xem RestTemplate javadoc :

LƯU Ý: Kể từ 5.0, tính năng không chặn, phản ứng org.springframework.web.reactive.client.WebClientmang đến một giải pháp thay thế hiện đại RestTemplatevới sự hỗ trợ hiệu quả cho cả đồng bộ hóa và không đồng bộ, cũng như các tình huống phát trực tuyến. Các RestTemplatesẽ hết hạn vào một phiên bản tương lai và sẽ không có các tính năng mới quan trọng bổ sung trong tương lai. Xem WebClientphần tài liệu tham khảo Spring Framework để biết thêm chi tiết và mã ví dụ.

Do đó, giải pháp tốt nhất sẽ là từ bỏ RestTemplateủng hộ WebClient.


Sử dụng WebClientcho luồng thông tin xác thực của khách hàng

Định cấu hình đăng ký khách hàng và nhà cung cấp theo chương trình hoặc sử dụng cấu hình tự động 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

... và 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;
}

Cấu hình WebClientthể hiện để sử dụng ServerOAuth2AuthorizedClientExchangeFilterFunctionvới cung cấp OAuth2AuthorizedClientManager:

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

Bây giờ, nếu bạn cố gắng thực hiện một yêu cầu bằng cách sử dụng WebClienttrường hợp này , trước tiên, nó sẽ yêu cầu mã thông báo từ máy chủ ủy quyền và đưa nó vào yêu cầu.


Cảm ơn, điều đó sẽ xóa một số thứ, nhưng từ tất cả các tài liệu được liên kết ở trên, tôi vẫn đang cố gắng tìm một ví dụ trong đó một thuật ngữ chặn (hoặc bất kỳ thuật ngữ mới nào WebClientđược sử dụng) hoặc một cái gì đó tương tự được sử dụng để lấy mã thông báo OAuth từ một nhà cung cấp OAuth tùy chỉnh (không phải là một trong những nhà cung cấp hỗ trợ OoTB như Facebook / Google) để thêm nó vào yêu cầu gửi đi. Tất cả các ví dụ dường như tập trung vào xác thực các yêu cầu đến với các nhà cung cấp khác. Bạn đã có bất kỳ con trỏ cho bất kỳ ví dụ tốt?
Matt Williams

1
@MattWilliams Tôi đã cập nhật câu trả lời với một ví dụ về cách sử dụng WebClientvới loại cấp giấy ủy nhiệm khách hàng.
Anar Sultanov

Hoàn hảo, tất cả bây giờ có ý nghĩa hơn nhiều, cảm ơn bạn rất nhiều. Tôi có thể không có cơ hội dùng thử trong vài ngày, nhưng chắc chắn sẽ quay lại và đánh dấu đây là câu trả lời đúng khi tôi đã đi
Matt Williams

1
Điều đó thật đáng thất vọng bây giờ quá lol ... ít nhất là UnAuthenticatedServerOAuth2AuthorizedClientRep repository là ...
Sledge Hammer

Cảm ơn @Sledge Hammer, tôi đã cập nhật câu trả lời của mình.
Anar Sultanov

1

Câu trả lời trên từ @Anar Sultanov đã giúp tôi đi đến điểm này, nhưng khi tôi phải thêm một số tiêu đề bổ sung vào yêu cầu mã thông báo OAuth của mình, tôi nghĩ rằng tôi sẽ cung cấp câu trả lời đầy đủ cho cách tôi giải quyết vấn đề cho trường hợp sử dụng của mình.

Cấu hình chi tiết nhà cung cấp

Thêm vào đây 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:}

Thực hiện tùy chỉnh ReactiveOAuth2AccessTokenResponseClient

Vì đây là giao tiếp giữa máy chủ với máy chủ, chúng tôi cần sử dụng ServerOAuth2AuthorizedClientExchangeFilterFunction. Điều này chỉ chấp nhận a ReactiveOAuth2AuthorizedClientManager, không phải là không phản ứng OAuth2AuthorizedClientManager. Do đó, khi chúng tôi sử dụng ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider()(để cung cấp cho nhà cung cấp sử dụng để thực hiện yêu cầu OAuth2), chúng tôi phải cung cấp cho nó ReactiveOAuth2AuthorizedClientProviderthay vì không phản ứng OAuth2AuthorizedClientProvider. Theo tài liệu tham khảo bảo mật mùa xuân nếu bạn sử dụng không phản ứng, DefaultClientCredentialsTokenResponseClientbạn có thể sử dụng .setRequestEntityConverter()phương pháp để thay đổi yêu cầu mã thông báo OAuth2, nhưng tương đương phản ứng WebClientReactiveClientCredentialsTokenResponseClientkhông cung cấp cơ sở này, vì vậy chúng tôi phải sử dụng riêng của chúng tôi (chúng tôi có thể sử dụng WebClientReactiveClientCredentialsTokenResponseClientlogic hiện có ).

Việc triển khai của tôi đã được gọi UaaWebClientReactiveClientCredentialsTokenResponseClient(việc thực hiện bị bỏ qua vì nó chỉ thay đổi rất ít các phương thức headers()body()phương thức từ mặc định WebClientReactiveClientCredentialsTokenResponseClientđể thêm một số trường tiêu đề / nội dung bổ sung, nó không thay đổi luồng xác thực bên dưới).

Cấu hình WebClient

Các ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient()phương pháp đã bị phản đối, vì vậy theo lời khuyên deprecation từ phương pháp đó:

Không dùng nữa Sử dụng ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager)thay thế. Tạo một thể hiện của ClientCredentialsReactiveOAuth2AuthorizedClientProvidercấu hình với một WebClientReactiveClientCredentialsTokenResponseClient(hoặc một tùy chỉnh) và cung cấp nó cho DefaultReactiveOAuth2AuthorizedClientManager.

Điều này kết thúc với cấu hình trông giống như:

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

Sử dụng WebClientnhư bình thường

Các oAuth2WebClienthạt đậu đã sẵn sàng để được sử dụng để truy cập tài nguyên được bảo vệ bởi nhà cung cấp OAuth2 cấu hình của chúng tôi theo cách bạn sẽ thực hiện bất kỳ yêu cầu khác bằng cách sử dụng WebClient.


Làm cách nào để vượt qua id khách hàng, bí mật khách hàng và điểm cuối oauth theo chương trình?
monti

Tôi đã không thử điều này, nhưng có vẻ như bạn có thể tạo các phiên bản của ClientRegistrations với các chi tiết cần thiết và chuyển chúng vào hàm tạo InMemoryReactiveClientRegistrationRepository(triển khai mặc định ReactiveClientRegistrationRepository). Sau đó, bạn sử dụng InMemoryReactiveClientRegistrationRepositoryhạt đậu mới được tạo ra thay cho quyền tự động của tôi clientRegistrationRepositoryđược truyền vào oauthFilteredWebClientphương thức
Matt Williams

Mh, nhưng tôi không thể đăng ký khác nhau ClientRegistrationtrong thời gian chạy, phải không? Theo như tôi hiểu thì tôi cần phải tạo ra một hạt đậu ClientRegistrationkhi khởi động.
monti

Ah ok, tôi nghĩ bạn chỉ muốn không khai báo chúng trong application.propertiestập tin. Việc tự triển khai ReactiveOAuth2AccessTokenResponseClientcho phép bạn thực hiện bất kỳ yêu cầu nào bạn muốn nhận mã thông báo OAuth2, nhưng tôi không biết làm thế nào bạn có thể cung cấp "bối cảnh" động cho mỗi yêu cầu. Điều tương tự cũng xảy ra nếu bạn triển khai toàn bộ bộ lọc của mình. sẽ cung cấp cho bạn quyền truy cập là yêu cầu gửi đi, vì vậy trừ khi bạn có thể suy ra những gì bạn cần từ đó. Tôi không chắc lựa chọn của bạn là gì. Trường hợp sử dụng của bạn là gì? Tại sao bạn không biết đăng ký có thể khi khởi động?
Matt Williams

1

Tôi thấy @matt Williams trả lời khá hữu ích. Mặc dù tôi muốn thêm vào trong trường hợp ai đó muốn lập trình vượt qua clientId và bí mật cho cấu hình WebClient. Đây là cách nó có thể được thực hiện.

 @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

Xin chào, có lẽ đã quá muộn tuy nhiên RestTemplate vẫn được hỗ trợ trong Spring Security 5, với ứng dụng không phản ứng RestTemplate vẫn được sử dụng, điều bạn phải làm là chỉ định cấu hình bảo mật mùa xuân đúng cách và tạo một bộ chặn như được đề cập trong hướng dẫn di chuyển

Sử dụng cấu hình sau để sử dụng luồng 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

Cấu hình để 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;
    }

}

Đánh chặn

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

Điều này sẽ tạo access_token trong cuộc gọi đầu tiên và bất cứ khi nào mã thông báo hết hạn. OAuth2AuthorizedClientManager sẽ quản lý tất cả điều này cho bạn

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.