【问题标题】:Spring Security 5 Replacement for OAuth2RestTemplateSpring Security 5 替换 OAuth2RestTemplate
【发布时间】:2020-03-17 19:41:06
【问题描述】:

spring-security-oauth2:2.4.0.RELEASE 中,OAuth2RestTemplateOAuth2ProtectedResourceDetailsClientCredentialsAccessTokenProvider 等类都已被标记为已弃用。

从这些类的 javadoc 中,它指向 spring security migration guide,暗示人们应该迁移到核心 spring-security 5 项目。但是,我很难找到如何在这个项目中实现我的用例。

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

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

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

此外,在上面链接的迁移指南中,还提到了 OAuth2AuthorizedClientService,它说它对于在拦截器中使用很有用,但这看起来又依赖于 ClientRegistrationRepository 之类的东西,这似乎是它维护注册的地方如果您想使用该提供来确保传入的请求经过身份验证,则适用于第三方提供程序。

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

【问题讨论】:

    标签: java spring-boot spring-security spring-security-oauth2


    【解决方案1】:

    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 将是 在未来的版本中已弃用,并且不会有主要的新功能 继续前进。请参阅 Spring Framework 的 WebClient 部分 参考文档了解更多细节和示例代码。

    因此,最好的解决方案是放弃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 实例发出请求,它将首先从授权服务器请求令牌并将其包含在请求中。

    【讨论】:

    • 现在已经过时了,哈哈……至少 UnAuthenticatedServerOAuth2AuthorizedClientRepository 是……
    • @AnarSultanov “因此,最好的解决方案是放弃 RestTemplate 转而使用 WebClient” 如果这不是一个选项,那该怎么办?例如,如果您计划向这些服务添加 OAuth 等安全性,Spring Cloud Discovery、Configuration 和 Feign 客户端仍然依赖 RestTemplate 和文档状态来提供自定义 RestTemplate。
    • @AnarSultanov 我已经尝试了您给出的确切示例,但出现 401 错误。似乎在尝试执行请求时没有进行身份验证。有什么建议吗?
    • @rafael.braga 如果没有看到所有代码和配置,我无法推荐任何东西。您可以尝试官方存储库中的示例并根据您的需要进行调整:github.com/spring-projects/spring-security/tree/master/samples/…
    • 这里是相关的 Spring Security 文档。提供更多详细信息和说明您可以配置 WebClient 的各种方式:docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/…
    【解决方案2】:

    嗨,也许为时已晚,但是 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 将为您管理所有这些

    【讨论】:

      【解决方案3】:

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

      【讨论】:

      • 上面的代码sn-p有没有可以测试的示例代码?
      • @SagarPilkhwal 您可以创建一个简单的基于 spring-security 的示例 Spring Boot 应用程序(您可以在网上轻松找到)。在那里设置基于 client_credentials 的访问权限并公开一个测试 API。然后您可以使用上面的代码创建 WebClient 并尝试调用该 API。
      【解决方案4】:

      @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。根据spring-security reference documentation,如果您使用非反应式DefaultClientCredentialsTokenResponseClient,您可以使用.setRequestEntityConverter() 方法来更改OAuth2 令牌请求,但反应式等效WebClientReactiveClientCredentialsTokenResponseClient 不提供此功能,因此我们必须实现我们的自己的(我们可以利用现有的WebClientReactiveClientCredentialsTokenResponseClient 逻辑)。

      我的实现被称为UaaWebClientReactiveClientCredentialsTokenResponseClient(实现被省略,因为它只是非常轻微地改变了默认WebClientReactiveClientCredentialsTokenResponseClient中的headers()body()方法以添加一些额外的标题/正文字段,它不会改变底层的身份验证流动)。

      配置WebClient

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

      已弃用。 请改用ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager)。创建一个配置了WebClientReactiveClientCredentialsTokenResponseClient(或自定义)的ClientCredentialsReactiveOAuth2AuthorizedClientProvider 实例,然后将其提供给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 bean 现在已准备好用于访问受我们配置的 OAuth2 提供程序保护的资源,就像您使用 WebClient 发出任何其他请求一样。

      【讨论】:

      • 如何以编程方式传递客户端 ID、客户端密码和 oauth 端点?
      • 我还没有尝试过,但看起来您可以使用所需的详细信息创建 ClientRegistrations 的实例,并将这些实例传递给 InMemoryReactiveClientRegistrationRepository 的构造函数(ReactiveClientRegistrationRepository 的默认实现)。然后,您使用新创建的InMemoryReactiveClientRegistrationRepository bean 代替传递给oauthFilteredWebClient 方法的自动连接的clientRegistrationRepository
      • Mh,但我无法在运行时注册不同的ClientRegistration,是吗?据我了解,我需要在启动时创建一个 ClientRegistration 的 bean。
      • 啊,好吧,我以为你只是不想在application.properties 文件中声明它们。实现自己的ReactiveOAuth2AccessTokenResponseClient 允许您发出任何想要获取 OAuth2 令牌的请求,但我不知道如何为每个请求提供动态“上下文”。如果您实现了自己的整个过滤器,情况也是如此. 所有这些都可以让您访问传出请求,所以除非您可以从那里推断出您需要什么,否则我不确定您的选择是什么。您的用例是什么?为什么您不知道启动时可能的注册?
      猜你喜欢
      • 1970-01-01
      • 2020-08-17
      • 1970-01-01
      • 2018-05-02
      • 2020-05-31
      • 2019-03-07
      • 2019-01-07
      • 2019-05-20
      • 2020-11-10
      相关资源
      最近更新 更多