【问题标题】:Feign and Spring Security 5 - Client CredentialsFeign 和 Spring Security 5 - 客户端凭据
【发布时间】:2020-11-26 16:23:40
【问题描述】:

我正在尝试调用一些后端系统,该系统由来自 Feign 客户端应用程序的 client_credentials 授权类型保护。

可以使用以下 curl 结构检索来自后端系统的访问令牌(仅作为示例):

curl --location --request POST '[SERVER URL]/oauth/grant' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Cookie: WebSessionID=172.22.72.1.1558614080219404; b8d49fdc74b7190aacd4ac9b22e85db8=2f0e4c4dbf6d4269fd3349f61c151223' \
--data-raw 'grant_type=client_credentials' \
--data-raw 'client_id=[CLIENT_ID]' \
--data-raw 'client_secret=[CLIENT_SECRET]'

{"accessToken":"V29C90D1917528E9C29795EF52EC2462D091F9DC106FAFD829D0FA537B78147E20","tokenType":"Bearer","expiresSeconds":7200}

这个 accessToken 然后应该设置在一个标头中,以便后续对后端系统的业务调用。

所以现在我的问题是,如何使用 Feign 和 Spring Boot Security 5 来实现这一点。 经过一些研究,我得出了这个解决方案(不起作用):

  1. 在 application.yml 中定义我的客户端:
spring:
  security:
    oauth2:
      client:
        registration:
          backend:
            client-id:[CLIENT_ID]
            client-secret: [CLIENT_SECRET]
            authorization-grant-type: client_credentials
    
        provider:
          backend:
            token-uri: [SERVER URL]/oauth/grant
  1. 创建一个 OAuth2AuthorizedClientManager Bean 以便能够授权(或重新授权)OAuth 2.0 客户端:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);

    return authorizedClientManager;
}
  1. 创建一个使用 OAuth2AuthorizedClientManager 的 Feign 请求拦截器:
public class OAuthRequestInterceptor implements RequestInterceptor {

    private OAuth2AuthorizedClientManager manager;

    public OAuthRequestInterceptor(OAuth2AuthorizedClientManager manager) {
        this.manager = manager;
    }

    @Override
    public void apply(RequestTemplate requestTemplate) {
        OAuth2AuthorizedClient client = this.manager.authorize(OAuth2AuthorizeRequest.withClientRegistrationId("backend").principal(createPrincipal()).build());
        String accessToken = client.getAccessToken().getTokenValue();
        requestTemplate.header(HttpHeaders.AUTHORIZATION, "Bearer" + accessToken);
    }

    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 "backend";
            }
        };
    }
}
  1. 创建一个使用拦截器的 FeignConfig:
public class FeignClientConfig {


    @Bean
    public OAuthRequestInterceptor repositoryClientOAuth2Interceptor(OAuth2AuthorizedClientManager manager) {
        return new OAuthRequestInterceptor(manager);
    }
}
  1. 这是我的 Feign 客户:
@FeignClient(name = "BackendRepository", configuration = FeignClientConfig.class, url = "${BACKEND_URL}")
public interface BackendRepository {

    @GetMapping(path = "/healthChecks", produces = MediaType.APPLICATION_JSON_VALUE)
    public Info healthCheck();
}

运行此代码时,出现错误:

org.springframework.web.client.UnknownContentTypeException: Could not extract response: no suitable HttpMessageConverter found for response type [class org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse] and content type [text/html;charset=utf-8]

调试代码看起来 DefaultClientCredentialsTokenResponseClient 正在使用基本身份验证请求身份验证端点。虽然我从来没有设置过。

有什么建议吗?也许有一种完全不同的方法可以做到这一点。

【问题讨论】:

  • 在切换到 Spring Security 5 之前,我们使用了 Spring Security OAuth2 包中的 ClientCredentialsAccessTokenProvider。这奏效了。

标签: spring-security oauth spring-cloud-feign


【解决方案1】:

为此,您需要拥有 Spring Security 5 和 Feign

  • 一个有效的 Spring Security 配置
  • 一个 Feign 拦截器
  • 使用该拦截器的 Feign 配置
  1. 工作 Spring 安全配置

在这里,我们将为您的 oauth2 client credentials 注册一个通用的 internal-api 客户端。这是您指定client-idclient-secretscopesgrant type 的地方。 所有基本的 Spring Security 5 东西。这还涉及设置提供程序(这里我使用了一个名为“yourprovider”的自定义 OpenID Connect 提供程序

spring:
  security:
    oauth2:
      client:
        registration:
          internal-api:
            provider: yourprovider
            client-id: x
            client-secret: y
            scope:
              - ROLE_ADMIN
            authorization-grant-type: client_credentials
        provider:
          yourprovider:
            issuer-uri: yourprovider.issuer-uri
      resourceserver:
        jwt:
          issuer-uri: yourprovider.issuer-uri

接下来你需要你的 feign 配置。这将使用OAuth2FeignRequestInterceptor

public class ServiceToServiceFeignConfiguration extends AbstractFeignConfiguration {

    @Bean
    public OAuth2FeignRequestInterceptor requestInterceptor() {
        return new OAuth2FeignRequestInterceptor(
                OAuth2AuthorizeRequest.withClientRegistrationId("internal-api")
                        .principal(new AnonymousAuthenticationToken("feignClient", "feignClient", createAuthorityList("ROLE_ANONYMOUS")))
                        .build());
    }
}

还有一个看起来像这样的 RequestInterceptor:

OAuth2AuthorizedClientManager 是一个可以在配置中配置的 bean

public OAuth2AuthorizedClientManager authorizedClientManager(final ClientRegistrationRepository clientRegistrationRepository, final OAuth2AuthorizedClientService authorizedClientService) {
    return new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
}

OAuth2AuthorizeRequest 由上面的 Feign 配置提供。 oAuth2AuthorizedClientManager 可以授权oAuth2AuthorizeRequest,获取访问令牌,并将其作为Authorization 标头提供给底层服务

public class OAuth2FeignRequestInterceptor implements RequestInterceptor {

    @Inject
    private OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager;

    private OAuth2AuthorizeRequest oAuth2AuthorizeRequest;

    OAuth2FeignRequestInterceptor(OAuth2AuthorizeRequest oAuth2AuthorizeRequest) {
        this.oAuth2AuthorizeRequest = oAuth2AuthorizeRequest;
    }

    @Override
    public void apply(RequestTemplate template) {
        template.header(AUTHORIZATION,getAuthorizationToken());
    }

    private String getAuthorizationToken() {
        final OAuth2AccessToken accessToken = oAuth2AuthorizedClientManager.authorize(oAuth2AuthorizeRequest).getAccessToken();
        return String.format("%s %s", accessToken.getTokenType().getValue(), accessToken.getTokenValue());
    }

}

【讨论】:

  • 非常感谢您的详细回答。但是,我没有得到一些东西:什么是类AbstractFeignConfiguration + 方法withClientRegistrationId() [是OAuth2AuthorizeRequest.withClientRegistrationId()?];同样在OAuth2FeignRequestInterceptor 中,authorizationHeader() 来自哪里?谢谢。
  • 更改了代码,确实是OAuth2AuthorizeRequest.withClientRegistrationId(使用静态导入),authorizationHeader 是这里的实用方法。为了清楚起见,现在将其删除
  • OK 很好...从AbstractFeignConfiguration 继承的没什么特别的?还是注释了?
  • AbstractFeignConfiguration 在我们的例子中只包含一些解码器/编码器。这里不需要
【解决方案2】:

我对 Feign 和 OAuth2 非常有经验,我花了好几个小时才找到如何做到这一点。 首先,假设我的应用基于最新的 Spring 库,所以我使用以下依赖项(spring-cloud-starter-openfeign 的托管版本为 3.0.0)

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
        <version>2.2.4.RELEASE</version>
    </dependency>

在我的 application.properties 我有以下内容

security.oauth2.client.access-token-uri=https://api.twitter.com/oauth2/token
security.oauth2.client.client-id=my-secret-twitter-id
security.oauth2.client.client-secret=my-secret-twitter-secret
security.oauth2.client.grant-type=client_credentials

最后是我的配置 bean

package es.spanishkangaroo.ttanalyzer.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;

import feign.RequestInterceptor;

@Configuration
public class FeignClientConfiguration {
    
    @Bean
    @ConfigurationProperties(prefix = "security.oauth2.client")
    public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
        return new ClientCredentialsResourceDetails();
    }

    @Bean
    public RequestInterceptor oauth2FeignRequestInterceptor(){
        return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails());
    }

    @Bean
    public OAuth2RestTemplate clientCredentialsRestTemplate() {
        return new OAuth2RestTemplate(clientCredentialsResourceDetails());
    }

}

那么 Feign 客户端就这么简单

package es.spanishkangaroo.ttanalyzer.api;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import es.clovelly.ttanalyzer.model.Trends;

@FeignClient(name = "twitterClient", url = "https://api.twitter.com/1.1/")
public interface TwitterClient {
 
    @GetMapping("/trends/place.json")
    Trends[] getTrendsById(@RequestParam Long id);
    
}

您可能已经注意到,代码在客户端调用之前会自动获取一个令牌(承载令牌)。如果您使用的是未过期的不记名令牌,则可以使用类似

@Bean
public OAuth2ClientContext oAuth2ClientContext() {
    DefaultOAuth2ClientContext context = new DefaultOAuth2ClientContext();
    context.setAccessToken(bearerToken);
    return context;
}

【讨论】:

【解决方案3】:

我试过你的方法。可惜没有成功。但这对我有用:Spring cloud Feign OAuth2 request interceptor is not working。看起来我现在使用了很多掠夺,但至少它确实有效。

【讨论】:

  • 感谢您的回答。这肯定有效,但它是 Spring Security OAuth2 的一部分,它已被弃用,取而代之的是 Spring Security 5.x spring.io/blog/2019/11/14/…。我真的很想让它在 Spring Security 5.x 中工作。
猜你喜欢
  • 2014-05-04
  • 2021-10-25
  • 2019-05-03
  • 2018-04-19
  • 2018-02-14
  • 2019-02-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多