Spring-security-oauth2-client 使用下游服务对我们自己进行身份验证

问题描述 投票:0回答:1

我什至不确定我敲的是对的门,所以请耐心听我说。我有一个服务,它被另一个服务(上游)调用并调用第三个服务(下游)。我的服务为上游提供 auth mech X,但由于该服务发生更改,应该使用 oauth2 调用下游。

假设我有一个如下所示的 HttpInterface:

@HttpExchange("/api")
public interface DownstreamClient {
    @GetExchange("/quotes/{id}")
    QouteResponse getQuoteById(
            @PathVariable("id") String id, 
            @RequestParam("type") String type);
}

为此,我在 app-{env}.yaml 配置文件中定义了属性:

downstream:
    location: "https://downstream.domain.com"
    oauth2:
        registration-id: "..."
        client-id: "..."
        client-name: "..."
        client-secret:
            key: "..."
            value: "..."
        redirectUri: "..."
        authorizationUri: "..."
        tokenUri: "..."
        scope: "..."

由此,我尝试构建一个由 WebClient 支持的 DownstreamClient 类型的 bean,该 WebClient 能够向下游验证服务本身,同时不接触包含“上游”身份验证的 SecurityContextHolder,并且是该目的所必需的。天真地,我尝试使用 spring-security-oauth2-client 使用 ServerOAuth2AuthorizedClientExchangeFilterFunction ,因为描述似乎与我的用例相关。我从那里开始,基本上做:

@Configuration
public class DownstreamClientConfiguration {
    @Bean
    DownstreamClient downstreamClient(WebClient downstreamClient) {
        return HttpServiceProxyFactory
                .builderFor(WebClientAdapter.create(downstreamClient))
                .build()
                .createClient(DownstreamClient.class);
    }

    @Bean
    WebClient downstreamClient(DownstreamProperties downstreamProps) {
        ClientRegistration clientRegistration = ClientRegistration.withRegId....;
        ReactiveClientRegistrationRepository clientRegistrationRepository = 
                new InMemoryReactiveClientRegistrationRepository(clientRegistration);
        ReactiveOAuth2AuthorizedClientService authorizedClientService = 
                new ReactiveOAuth2AuthorizedClientService(clientRegistrationRepository);
        ReactiveOAuth2AuthorizedClientManager authorizedClientManager =
                new ReactiveOAuth2AuthorizedClientManager(
                        clientRegistrationRepository, authorizedClientService);
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Filter = 
                new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth2Filter.setDefaultClientRegistrationId(downstreamProps.oauth2().registrationId());
        return WebClient.builder().filter(oauth2Filter).baseUrl(downstreamProps.location()).build();
    }
}

嗯,我想沿着这些思路似乎就足够了。然后,我了解到每次我从授权服务器收到“unauthorized_client”时,我就不断地挖掘,发现了一些看起来很重要的信息,而这些信息在本主题的 spring.io 文档中并不太清楚。也就是说我需要打电话:

.attribute(oauth2AuthorizedClient(authorizedClient))

其中authorizedClient基于我当前非常有限关于这个主题的知识来自我添加一个控制器参数:

@RegisteredOAuth2AuthorizedClient(registrationId = "...") 
OAuth2AuthorizedClient authorizedClient

由于所有这些流程都发生在服务层(@Service bean)中,并且我试图向下游验证“我自己”(服务本身正在运行),甚至没有查看我自己的服务的安全实现,并且此注释不似乎对 @Service 注解的 bean 方法没有影响:

@Service
public class MyNaiveServiceExample {
    @Autowired private DownstreamClient downstreamClient;

    public Optional<QouteDTO> getQouteById(
            @RegisteredOAuth2AuthorizedClient("...")
            OAuth2AuthorizedClient authorizedClient,
            String id) {
        QouteReponse response = downstreamClient(authorizedClient, id, "quoteId");
        return Optional.of(QuoteDTO.from(response));
    }
}

简而言之,这就是我的用例。但这显然不起作用,所以我检查了 OAuth2AuthorizedClientService 或 OAuth2AuthorizedClientProvider (即 ClientCredentialsOAuth2AuthorizedClientProvider 因为我有客户端秘密)。

OAuth2AuthorizedClientService 似乎也不是可行的方法,因为:

T loadAuthorizedClient(字符串 clientRegistrationId, 字符串principalName) 返回与提供的客户端注册标识符和最终用户主体名称关联的 OAuth2AuthorizedClient,如果不可用,则返回 null。

ClientCredentialsOAuth2AuthorizedClientProvider 看起来更像是我需要的,但说实话,我不知道获取 OAuth2AuthorizedClient 实例所需的预期 OAuth2AuthorizationContext 可能是什么。

我不需要……甚至不用看经过身份验证的主体,自己做!尽管如此,我到底如何在代理后面的 WebClient 上设置该属性似乎仍然存在问题。我想,我可以使用类似的东西:

@GetExchange("/quotes/{id}")
QouteResponse getQuoteById(
        @PathVariable("id") String id, 
        @RequestParam("type") String type,
        @RequestAttribute("org.springframework.security.oauth2.client.OAuth2AuthorizedClient") WebClient authorizedClient);

看起来很可怕,但这就是交换过滤器函数用于解析客户端的属性的名称。我想做的...可以使用 spring-security-oauth2-client 来完成吗?

spring-security spring-security-oauth2
1个回答
0
投票

首先,由于您似乎正在使用 servlet,因此

RestClient
(同步)可能比
WebClient
(反应式)更适合。如果您想坚持使用
WebClient
,请将
OAuth2ClientHttpRequestInterceptor
替换为
ServerOAuth2AuthorizedClientExchangeFilterFunction
(在反应式应用程序中)或
ServletOAuth2AuthorizedClientExchangeFilterFunction
(在 servlet 中)。

其次,如果您使用

spring-boot-starter-oauth2-client
和 Spring Boot 属性,会更容易:

spring:
  security:
    oauth2:
      client:
        provider:
          sso:
           issuer-uri: ${issuer}
        registration:
          downstream-api-registration:
            provider: sso
            authorization-grant-type: client_credentials
            client-id: chouette-api
            client-secret: change-me
            scope: openid

这将注册您需要的授权客户存储库和管理器。

最后,为了避免

RestClient
实例和
@HttpExchange
实现之间的混淆,我将其重命名为
DownstreamApi
:

@HttpExchange("/api")
public interface DownstreamApi {
  @GetExchange("/quotes/{id}")
  QouteResponse getQuoteById(@PathVariable("id") String id, @RequestParam("type") String type);
}

仅使用“官方”库

假设你已经定义了这样一个属性:

downstream-api-base-uri: http://localhost:8180

您可以按如下方式定义

RestClient
bean:

@Bean
RestClient downstreamRestClient(@Value("downstream-api-base-uri") URI downstreamApiBaseUri,
    OAuth2AuthorizedClientManager authorizedClientManager,
    OAuth2AuthorizedClientRepository authorizedClientRepository) {
  return RestClient.builder().baseUrl(downstreamApiBaseUri)
      .requestInterceptor(registrationClientHttpRequestInterceptor(authorizedClientManager,
          authorizedClientRepository, "(downstream-api-registration"))
      .build();
}

ClientHttpRequestInterceptor registrationClientHttpRequestInterceptor(
    OAuth2AuthorizedClientManager authorizedClientManager,
    OAuth2AuthorizedClientRepository authorizedClientRepository, String registrationId) {
  final var interceptor = new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
  interceptor.setClientRegistrationIdResolver((HttpRequest request) -> registrationId);
  interceptor.setAuthorizationFailureHandler(
      OAuth2ClientHttpRequestInterceptor.authorizationFailureHandler(authorizedClientRepository));
  return interceptor;
}

您可以让 Spring 使用上面定义的 REST 客户端 bean 生成上述内容的实现,如下所示:

@Bean
DownstreamApi downstreamApi(RestClient downstreamRestClient) {
  return HttpServiceProxyFactory.builderFor(RestClientAdapter.create(downstreamRestClient)).build()
      .createClient(DownstreamApi.class);
}

瞧!您可以在您喜欢的任何

downstreamApi
中自动连接此
@Component

spring-addons-starter-rest

我维护 Spring Boot Strater 以简化

RestClient
WebClient
配置
。有了它,您可以使用 OAuth2 客户端凭据流程配置请求授权,如下所示(保留客户端的“官方”启动属性
registration
):

com:
  c4-soft:
    springaddons:
      rest:
        client:
          downstream-rest-client:
            base-url: http://localhost:8180
            authorization:
              oauth2:
                oauth2-registration-id: downstream-api-registration
@Bean
DownstreamApi downstreamApi(RestClient downstreamRestClient) throws Exception {
  return new RestClientHttpExchangeProxyFactoryBean<>(DownstreamApi.class, downstreamRestClient).getObject();
}

使用应用程序属性的配置选项包括:

  • 其他授权机制(
    Basic
    、静态 API 密钥或在资源服务器的安全上下文中转发令牌)
  • HTTP 代理配置(这可能非常棘手,特别是如果您想使用“标准”
    HTTP_PROXY
    NO_PROXY
    环境变量)
  • 以及更多类似块大小或超时
© www.soinside.com 2019 - 2024. All rights reserved.