Spring Security 5替换OAuth2RestTemplate


14

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

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

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

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

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

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

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

Answers:


15

Spring Security 5.2.x的OAuth 2.0客户端功能不支持RestTemplate,仅支持WebClient。参见Spring Security Reference

HTTP客户端支持

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

此外,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与客户端凭据授予类型一起使用。
Anar Sultanov

太好了,现在一切都变得有意义了,非常感谢。我可能几天都没有机会尝试一下,但是一旦去了,一定会再回来并将其标记为正确答案
Matt Williams

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。这仅接受a ReactiveOAuth2AuthorizedClientManager,而不接受非反应性OAuth2AuthorizedClientManager。因此,当我们使用ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider()(向它提供提供程序以使用它来发出OAuth2请求)时,我们必须给它一个,ReactiveOAuth2AuthorizedClientProvider而不是非反应式OAuth2AuthorizedClientProvider。根据spring-security参考文档,如果您使用非反应式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提供商在你使用任何其他请求使保护方式访问资源WebClient


如何以编程方式传递客户端ID,客户端秘密和oauth端点?
monti

我没有尝试过,但是看起来您可以创建ClientRegistration具有所需详细信息的s 实例, 并将其传递到的构造函数中InMemoryReactiveClientRegistrationRepository(作为的默认实现ReactiveClientRegistrationRepository)。然后,您可以使用新创建的InMemoryReactiveClientRegistrationRepositorybean代替clientRegistrationRepository传递给该oauthFilteredWebClient方法的自动装配
Matt Williams

嗯,但是我无法ClientRegistration在运行时注册其他内容,对吗?据我了解,我需要ClientRegistration在启动时创建一个bean 。
monti

好的,我想您只是不想在application.properties文件中声明它们。实现自己的ReactiveOAuth2AccessTokenResponseClient过滤器可以使您提出任何想要获取OAuth2令牌的请求,但是我不知道如何为每个请求为其提供动态的“上下文”。如果您实现自己的整个过滤器,情况也是如此。会允许您访问传出的请求,因此除非您可以从那里推断出您的需求,否则我不确定您的选择是什么?用例是什么?为什么您不知道启动时可能的注册?
Matt Williams

1

我发现@matt Williams的回答很有帮助。尽管我想补充一下,以防有人希望以编程方式传递clientId和secret以进行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

嗨,也许为时已晚,但是Spring Security 5仍支持RestTemplate,仍然使用非反应性应用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将为您管理所有这一切

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.