Spring Security 5替换OAuth2RestTemplate

Mat*_*ams 7 java spring-security spring-boot spring-security-oauth2

在,和spring-security-oauth2:2.4.0.RELEASE等类中OAuth2RestTemplate,所有这些都已被标记为已弃用。OAuth2ProtectedResourceDetailsClientCredentialsAccessTokenProvider

从这些类的javadoc指向Spring安全迁移指南,该指南暗示人们应该迁移到核心spring-security 5项目。但是,我在寻找如何在该项目中实现用例时遇到了麻烦。

如果您希望对应用程序的传入请求进行身份验证并且想要使用第三方OAuth提供程序来验证身份,则所有文档和示例都讨论了与第三方OAuth提供程序集成的问题。

在我的用例中,我要做的就是RestTemplate向受OAuth保护的外部服务发出请求。目前OAuth2ProtectedResourceDetails,我使用客户ID和密码创建了一个,并将其传递给OAuth2RestTemplate。我还ClientCredentialsAccessTokenProvider向中添加了一个自定义,该自定义OAuth2ResTemplate仅将一些额外的标头添加到我正在使用的OAuth提供程序所需的令牌请求中。

在spring-security 5文档中,我找到了提到自定义令牌请求的部分,但又一次看起来是在与第三方OAuth提供者验证传入请求的上下文中。目前尚不清楚如何将其与类似的东西结合使用,ClientHttpRequestInterceptor以确保对外部服务的每个传出请求都首先获得令牌,然后再将令牌添加到请求中。

同样,在上面链接的迁移指南中,引用了一个OAuth2AuthorizedClientService,它说对在拦截器中使用很有用,但是这看起来又像是依赖于这样的东西ClientRegistrationRepository,如果您想使用,它似乎在其中为第三方提供商维护注册。提供以确保对传入请求进行身份验证。

有什么方法可以利用spring-security 5中的新功能来注册OAuth提供程序,以便获得令牌以添加到应用程序的传出请求中?

Ana*_*nov 42

Spring Security 5.2.x 的 OAuth 2.0 Client features 不支持RestTemplate,但仅支持WebClient. 请参阅Spring 安全参考

HTTP 客户端支持

  • WebClient Servlet 环境的集成(用于请求受保护的资源)

此外,RestTemplate将在未来版本中弃用。请参阅RestTemplate javadoc

注意:从 5.0 开始,非阻塞、反应式 org.springframework.web.reactive.client.WebClientRestTemplate同步和异步以及流场景提供了有效支持的现代替代方案。该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
Run Code Online (Sandbox Code Playgroud)

...?还有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;
}
Run Code Online (Sandbox Code Playgroud)

配置WebClient实例以ServerOAuth2AuthorizedClientExchangeFilterFunction与提供的一起使用OAuth2AuthorizedClientManager

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

现在,如果您尝试使用此WebClient实例发出请求,它将首先从授权服务器请求令牌并将其包含在请求中。

  • @AnarSultanov“因此,最好的解决方案是放弃 RestTemplate,转而使用 WebClient”,那么在无法选择的地方呢?例如,如果您计划向这些服务添加 OAuth 等安全性,Spring Cloud Discovery、Configuration 和 Feign 客户端仍然依赖 RestTemplate 和文档状态来提供自定义 RestTemplate。 (2认同)

Lea*_*sis 9

嗨,也许为时已晚,但是 Spring Security 5 仍然支持 RestTemplate,对于非反应性应用程序 RestTemplate 仍然使用,您需要做的只是正确配置 Spring Security 并创建一个拦截器,如迁移指南中所述

使用以下配置来使用 client_credentials 流

应用程序.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
Run Code Online (Sandbox Code Playgroud)

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

}
Run Code Online (Sandbox Code Playgroud)

拦截器

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();
            }
        };
    }
}
Run Code Online (Sandbox Code Playgroud)

这将在第一次调用和令牌过期时生成 access_token。OAuth2AuthorizedClientManager 将为您管理所有这些


Jog*_*ger 6

我发现@matt Williams 的回答很有帮助。虽然我想添加以防有人想以编程方式为 WebClient 配置传递 clientId 和 secret。这是如何完成的。

 @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);
    }
}
Run Code Online (Sandbox Code Playgroud)


Mat*_*ams 5

@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:}
Run Code Online (Sandbox Code Playgroud)

实施定制ReactiveOAuth2AccessTokenResponseClient

由于这是服务器到服务器的通信,我们需要使用ServerOAuth2AuthorizedClientExchangeFilterFunction. 这只接受 a ReactiveOAuth2AuthorizedClientManager,而不接受非反应性OAuth2AuthorizedClientManager。因此,当我们使用ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider()(为其提供用于发出 OAuth2 请求的提供程序)时,我们必须为其提供 aReactiveOAuth2AuthorizedClientProvider而不是非响应式OAuth2AuthorizedClientProvider. 根据spring-security 参考文档,如果您使用非反应式,DefaultClientCredentialsTokenResponseClient则可以使用该.setRequestEntityConverter()方法来更改 OAuth2 令牌请求,但反应式等效方法WebClientReactiveClientCredentialsTokenResponseClient不提供此功能,因此我们必须实现自己的(我们可以利用现有的WebClientReactiveClientCredentialsTokenResponseClient逻辑)。

我的实现被调用(省略了实现,因为它仅从默认值中UaaWebClientReactiveClientCredentialsTokenResponseClient稍微改变了headers()和方法以添加一些额外的标头/正文字段,它不会更改底层身份验证流程)。body()WebClientReactiveClientCredentialsTokenResponseClient

配置WebClient

ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient()方法已被弃用,因此请遵循该方法的弃用建议:

已弃用。代替使用。创建一个配置有(或自定义)ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager)的实例,然后将其提供给.ClientCredentialsReactiveOAuth2AuthorizedClientProviderWebClientReactiveClientCredentialsTokenResponseClientDefaultReactiveOAuth2AuthorizedClientManager

最终的配置看起来像这样:

@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();
}
Run Code Online (Sandbox Code Playgroud)

WebClient正常使用

oAuth2WebClientbean 现在已准备好用于访问受我们配置的 OAuth2 提供程序保护的资源,就像您使用WebClient.