微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

如何使用 Spring Security 为 client_credentials 工作流向 Feign 客户端提供 OAuth2 令牌 深入示例

如何解决如何使用 Spring Security 为 client_credentials 工作流向 Feign 客户端提供 OAuth2 令牌 深入示例

概述

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

我的应用程序使用 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 客户端的请求中,方法添加一个 Authorization Bearer 标头。我的假设是我可以使用 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)

我还尝试了一些在 SO 上找到的方法

我的假设是我可以以某种方式使用 Spring Security 5 来解决这个问题,但我根本无法理解如何实际做到这一点。在我看来,我发现的大多数教程和代码示例实际上都需要用户会话,或者在 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 called”输出,然后是一个异常:

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'

解决方法

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

只需将 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
}
,

根据文档需要使用 AuthorizedClientServiceOAuth2AuthorizedClientManager 而不是 DefaultOAuth2AuthorizedClientManager

在 HttpServletRequest 上下文之外操作时,请改用 AuthorizedClientServiceOAuth2AuthorizedClientManager。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。