具有密码授予类型的 WebClient 的 Spring Security OAuth 客户端不要求新令牌

Posted

技术标签:

【中文标题】具有密码授予类型的 WebClient 的 Spring Security OAuth 客户端不要求新令牌【英文标题】:Spring Security OAuth Client for WebClient With Password Grant Type Doesn't Ask For New Token 【发布时间】:2020-10-31 08:21:36 【问题描述】:

我有一个 Spring Web 应用程序,它有一个为其 API 端点配置的 oauth2 资源服务器和一个完全不同的 oauth2 客户端,用于它进行的 REST 调用。 oauth2 客户端需要是密码授权类型。用户名和密码是固定的(不是来自 HTTP 请求)。我的问题是 30 分钟后访问令牌和刷新令牌都过期了,所以没有办法进行刷新。我希望 Spring Security 只会要求新的访问令牌,但不会。它使用过期的端点调用 REST 端点并返回 403。 这是我所拥有的:

application.yml:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://our.idp.keycloak.host/auth/realms/firstrealm
      client:
        registration:
          my-client-authorization:
            client-id: my_client
            client-secret: $CLIENT_SECRET
            authorization-grant-type: password
            scope: openid, profile
        provider:
          my-client-authorization:
            token-uri: https://our.idp.keycloak.host/auth/realms/secondrealm/protocol/openid-connect/token

MyClientConfig.java:

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.Map;

@Configuration
@RequiredArgsConstructor
public class MyClientConfig 
    @Bean
    WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) 
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth2Client.setDefaultClientRegistrationId("my-client-authorization");
        return WebClient.builder()
                .apply(oauth2Client.oauth2Configuration())
                .baseUrl("https://the.api.host.to.call")
                .build();
    

    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientRepository authorizedClientRepository
    ) 
        OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
                .password()
                .build();

        DefaultOAuth2AuthorizedClientManager result = new DefaultOAuth2AuthorizedClientManager(
                clientRegistrationRepository,
                authorizedClientRepository
        );

        result.setAuthorizedClientProvider(authorizedClientProvider);
        result.setContextAttributesMapper(oAuth2AuthorizeRequest -> Map.of(
                OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "user",
                OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password"
        ));

        return result;
    

API 调用本身:

private <T> T callApi(Function<UriBuilder, URI> uriFunction, Class<T> resultType) 
    return this.webClient
            .get()
            .uri(uriFunction)
            .retrieve()
            .bodyToMono(resultType)
            .block();

我第一次调用它时它就起作用了。但是 30 分钟后,令牌已经死了,我不知道如何获得一个新的。 如果我将其切换为 client_credentials 授权类型,它会起作用,它会在需要时自动获取新令牌。但由于某种原因,我不能对密码授予类型做同样的事情。

编辑: 所以我设法解决了这个问题:https://github.com/spring-projects/spring-security/issues/8831。 当我也为 refreshToken 配置 WebClient 时,它会在刷新令牌过期时崩溃。但是当你之后再次发出请求时,它会获得一个新的令牌。所以我不得不将 API 调用包装在 try catch 中,如果错误是我关心的错误,我会再次调用 API。这不是一个非常优雅的解决方案,但它确实有效。

【问题讨论】:

我的回答能解决你的问题吗? 【参考方案1】:

明确提到here,Spring会在访问令牌过期时自动刷新。

我认为您需要您使用过的“反应性”等效类。请试试这个sn-p。

@Configuration
@RequiredArgsConstructor
public class MyClientConfig 
  @Bean
  WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) 
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth2Client.setDefaultClientRegistrationId("my-client-authorization");
    return WebClient.builder()
        .filter(oauth2Client)
        .baseUrl("https://the.api.host.to.call")
        .build();
  

  @Bean
  public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
      ReactiveClientRegistrationRepository clientRegistrationRepository,
      ReactiveOAuth2AuthorizedClientService authorizedClientService
  ) 
    ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
        .password()
        .build();

    AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager result = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
        clientRegistrationRepository,
        authorizedClientService
    );

    result.setAuthorizedClientProvider(authorizedClientProvider);
    result.setContextAttributesMapper(oAuth2AuthorizeRequest -> Mono.just(Map.of(
        OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "user",
        OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password"
    )));

    return result;
  

【讨论】:

您好,感谢您的回答。我试了一下,得到:方法 MyClientConfig 的参数 0 需要一个找不到的 'org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository' 类型的 bean。问题是我没有将 WebFlux 与 WebFluxSecurity 用于主要应用程序端点,而是使用普通的 WebSecurity?如果我重新实现它会有帮助吗? @Filip 如果您通过此属性注册了您的客户端:spring.security.oauth2.client.registration,那么InMemoryReactiveClientRegistrationRepository bean 将通过ReactiveOAuth2ClientConfigurations 注册。另外我建议不要将 web 和 webflux 混合在一起。 ReactiveOAuth2AuthorizedClientService 设置在哪里?您是否在某个地方有一个示例,其中包含所有设置,包括配置?他们似乎在任何地方都不是一个很好的例子。【参考方案2】:

对Abhinaba Chakraborty 帖子的补充:

没有定义“ReactiveOAuth2AuthorizedClientService”,也没有“clientRegistrationRepository”。根据您的帖子,我有这些工作设置

@Bean
ReactiveClientRegistrationRepository getRegistrationRepository() 
    ClientRegistration registration = ClientRegistration
            .withRegistrationId(REGISTRATION_ID)
            .tokenUri(TOKEN_URI)
            .clientId(CLIENT_ID)
            .clientSecret(CLIENT_SECRET)
            .authorizationGrantType(new AuthorizationGrantType(GRANT_TYPE))
            .build();
    return new InMemoryReactiveClientRegistrationRepository(registration);


@Bean
WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) 
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth2Client.setDefaultClientRegistrationId(REGISTRATION_ID);
    return WebClient.builder()
            .filter(oauth2Client)
            .build();


@Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
        ReactiveClientRegistrationRepository clientRegistrationRepository
) 
    ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
            .password()
            .build();

    AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager clientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
            clientRegistrationRepository,
            new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository)
    );

    clientManager.setAuthorizedClientProvider(authorizedClientProvider);
    clientManager.setContextAttributesMapper(oAuth2AuthorizeRequest -> Mono.just(Map.of(
            OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, USERNAME,
            OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, PASSWORD
    )));

    return clientManager;

所有常量都应该清楚。 REGISTRATION_ID 只是任何值,但在创建/使用存储库时需要相等

【讨论】:

以上是关于具有密码授予类型的 WebClient 的 Spring Security OAuth 客户端不要求新令牌的主要内容,如果未能解决你的问题,请参考以下文章

Oauth 刷新令牌授予类型

如何授予用户角色具有授予选项的权限?

如何在使用 OAuth2 的资源所有者密码凭据授予类型时对客户端凭据保密

Laravel Passport 密码授予刷新令牌

OAuth2 密码授予和基本身份验证

具有所有 4 种授权类型的 Oauth2 服务器示例