如何使用Spring Security for CLIENT_Credentials工作流向伪装客户端提供OAuth2令牌 [英] How to provide an OAuth2 token to a Feign client using Spring Security for the client_credentials workflow

查看:14
本文介绍了如何使用Spring Security for CLIENT_Credentials工作流向伪装客户端提供OAuth2令牌的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

概述

我正在尝试编写一个访问公共REST API的程序。为了使我能够使用它,我需要提供OAuth2令牌。

我的App使用的是Spring Boot 2.4.2和Spring Cloud版本2020.0.1。该应用程序本身确实每24小时调用一次REST API,下载数据并将其存储在数据库中。另一个微服务在其他点使用此数据,并且需要每天刷新此数据。

我的方法是使用OpenFeign声明使用REST API的REST客户端,并为其提供OAuth2令牌。这是一个非常常见的问题,因此我假设机器对机器client_credentials工作流有很好的文档记录。

确实,我找到了一个使用OpenFeign实现此操作的简单示例--此处:https://github.com/netshoes/sample-feign-oauth2-interceptor/blob/master/src/main/java/com/sample/feign/oauth2/interceptor/OrderFeignClientConfiguration.java

TL;DR:正在尝试编写需要OAuth2令牌(CLIENT_Credentials授权类型)的计算机到计算机微服务。

问题

这是我的第一次尝试,但不幸的是,在新的Spring Security发行版中,我似乎无法实例化OAuth2FeignRequestInterceptor,我可能会遇到包问题。然后,我继续研究了Spring Security和新的OAuth2重写的文档,可以在这里找到:https://docs.spring.io/spring-security/site/docs/5.1.2.RELEASE/reference/htmlsingle/#oauth2client

接近

我的方法是使用RequestInterceptor,它通过添加授权承载标头将当前OAuth2令牌注入到OpenFeign客户端的请求中。我的假设是,我可以使用Spring Security OAuth2层或多或少地自动检索它。

使用文档,我尝试为我的拦截器提供一个OAuth2RegisteredClient的Bean,以及一个OAuth2AccessToken类型的Bean--两者都不起作用。我的最后一次尝试是这样的,看起来像是一种万福玛丽,一种方法:

    @Bean
    public OAuth2AccessToken apiAccessToken(
            @RegisteredOAuth2AuthorizedClient("MY_AWESOME_PROVIDER") OAuth2AuthorizedClient authorizedClient) {
        return authorizedClient.getAccessToken();
    }
这不起作用,因为RegisteredOAuth2AuthorizedClient需要一个用户会话,以免它是null。我也在Stackoverflow上看到其他人在尝试同样的方法,但他们实际上是Controller(=>;Resolving OAuth2AuthorizedClient as a Spring bean)

我还尝试了一些我在这里找到的方法,因此:

我的假设是我可以以某种方式使用Spring Security5来解决这个问题,但我就是不能理解如何实际做到这一点。在我看来,我找到的大多数教程和代码示例实际上都需要一个用户会话,或者在Spring Security 5中已经过时了。

我似乎真的遗漏了一些东西,我希望有人能给我指个正确的方向,指向如何实现这一点的教程或书面文档。

深入示例

我尝试提供一个OAuth2AuthorizedClientManager,如本例所示(https://github.com/jgrandja/spring-security-oauth-5-2-migrate)。 为此,我按照示例代码注册了一个OAuth2AuthorizedClientManager

    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,
                                                                 OAuth2AuthorizedClientRepository authorizedClientRepository) {
        OAuth2AuthorizedClientProvider authorizedClientProvider =
                OAuth2AuthorizedClientProviderBuilder.builder()
                        .authorizationCode()
                        .refreshToken()
                        .clientCredentials()
                        .password()
                        .build();
        DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
                clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

并提供给我的RequestInterceptor,如下所示:

    @Bean
    public RequestInterceptor requestInterceptor(OAuth2AuthorizedClientManager clientManager) {
        return new OAuthRequestInterceptor(clientManager);
    }

最后我编写了拦截器,如下所示:

    private String getAccessToken() {
        OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest.withClientRegistrationId(appClientId)
                // .principal(appClientId) // if this is not set, I receive "principal cannot be null" (or empty)
                .build();
        return Optional.ofNullable(authorizedClientManager)
                .map(clientManager -> clientManager.authorize(request))
                .map(OAuth2AuthorizedClient::getAccessToken)
                .map(AbstractOAuth2Token::getTokenValue)
                .orElseThrow(OAuth2AccessTokenRetrievalException::failureToRetrieve);
    }

    @Override
    public void apply(RequestTemplate template) {
        log.debug("FeignClientInterceptor -> apply CALLED");
        String token = getAccessToken();
        if (token != null) {
            String bearerString = String.format("%s %s", BEARER, token);
            template.header(HttpHeaders.AUTHORIZATION, bearerString);
            log.debug("set the template header to this bearer string: {}", bearerString);
        } else {
            log.error("No bearer string.");
        }
    }

当我运行代码时,我可以在控制台中看到";FeignClientInterceptor->;Apply Call&Quot;输出,后面跟着一个异常:

Caused by: java.lang.IllegalArgumentException: servletRequest cannot be null

我的假设是我收到了这个消息,因为我没有活动的用户会话。因此,在我看来,我绝对需要一个来解决这个问题--我在机器对机器的通信中没有这个问题。

这是一个常见的用例,所以我确信我一定是在某个点上犯了错误。

使用的包

可能我的包裹弄错了?

    implementation 'org.springframework.boot:spring-boot-starter-amqp'
    implementation 'org.springframework.boot:spring-boot-starter-jooq'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

推荐答案

so。我在我的空闲时间玩你的解决方案。并找到了简单的解决方案:

只需在代码中添加SecurityContextHolder.getContext().authentication原则OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest.withClientRegistrationId(appClientId).build();

应该是这样的:

val request = OAuth2AuthorizeRequest
                .withClientRegistrationId("keycloak") // <-- here your registered client from application.yaml
                .principal(SecurityContextHolder.getContext().authentication)
                .build()

二手包:

implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")

application.yaml

spring:
  security:
    oauth2:
      client:
        registration:
          keycloak: # <--- It's your custom client. I am using keycloak
            client-id: ${SECURITY_CLIENT_ID}
            client-secret: ${SECURITY_CLIENT_SECRET}
            authorization-grant-type: client_credentials
            scope: openid # your scopes
        provider:
          keycloak: # <--- Here Registered my custom provider
            authorization-uri: ${SECURITY_HOST}/auth/realms/${YOUR_REALM}/protocol/openid-connect/authorize
            token-uri: ${SECURITY_HOST}/auth/realms/${YOUR_REALM}/protocol/openid-connect/token

feign:
  compression:
    request:
      enabled: true
      mime-types: application/json
    response:
      enabled: true
  client.config.default:
    connectTimeout: 1000
    readTimeout: 60000
    decode404: false
    loggerLevel: ${LOG_LEVEL_FEIGN:basic}

SecurityConfiguration

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfiguration() : WebSecurityConfigurerAdapter() {

    @Throws(Exception::class)
    override fun configure(http: HttpSecurity) {
        // @formatter:off
        http
                .authorizeRequests { authorizeRequests ->
                    authorizeRequests
                            .antMatchers(HttpMethod.GET, "/test").permitAll() // Here my public endpoint which do logic with secured client enpoint
                            .anyRequest().authenticated()
                }.cors().configurationSource(corsConfigurationSource()).and()
                .csrf().disable()
                .cors().disable()
                .httpBasic().disable()
                .formLogin().disable()
                .logout().disable()
                .oauth2Client()
        // @formatter:on
    }

    @Bean
    fun authorizedClientManager(
            clientRegistration: ClientRegistrationRepository?,
            authorizedClient: OAuth2AuthorizedClientRepository?
    ): OAuth2AuthorizedClientManager? {
        val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder
                .builder()
                .clientCredentials()
                .build()
        val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(clientRegistration, authorizedClient)
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
        return authorizedClientManager
    }

}

FeignClientConfiguration

private val logger = KotlinLogging.logger {}

class FeignClientConfiguration(private val authorizedClientManager: OAuth2AuthorizedClientManager) {

    @Bean
    fun requestInterceptor(): RequestInterceptor = RequestInterceptor { template ->
        if (template.headers()["Authorization"].isNullOrEmpty()) {
            val accessToken = getAccessToken()
            logger.debug { "ACCESS TOKEN TYPE: ${accessToken?.tokenType?.value}" }
            logger.debug { "ACCESS TOKEN: ${accessToken?.tokenValue}" }
            template.header("Authorization", "Bearer ${accessToken?.tokenValue}")
        }
    }

    private fun getAccessToken(): OAuth2AccessToken? {
        val request = OAuth2AuthorizeRequest
                .withClientRegistrationId("keycloak") // <- Here you load your registered client
                .principal(SecurityContextHolder.getContext().authentication)
                .build()
        return authorizedClientManager.authorize(request)?.accessToken
    }

}

TestClient

@FeignClient(
        name = "test",
        url = "http://localhost:8080",
        configuration = [FeignClientConfiguration::class]
)
interface TestClient {
    @GetMapping("/test")
    fun test(): ResponseEntity<Void> // Here my secured resource server endpoint. Expect 204 status
}

这篇关于如何使用Spring Security for CLIENT_Credentials工作流向伪装客户端提供OAuth2令牌的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆