如何使用Spring Security for CLIENT_Credentials工作流向伪装客户端提供OAuth2令牌 [英] How to provide an OAuth2 token to a Feign client using Spring Security for the client_credentials workflow
问题描述
概述
我正在尝试编写一个访问公共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)我还尝试了一些我在这里找到的方法,因此:
- Feign and Spring Security 5 - Client Credentials(提供的答案使用Spring Boot 2.2.4-因此不再相关)
- Alternative For OAuth2FeignRequestInterceptor as it is deprecated NOW另一位男士正在寻找
OAuth2FeignRequestInterceptor
的替代人选 - OAuth2FeignRequestInterceptor class deprecated in Spring Boot 2.3-此处的解决方案再次需要活动的用户会话
- https://github.com/jgrandja/spring-security-oauth-5-2-migrate这个Github回购不时出现,我研究了一下,但我认为它与我的问题无关--也许我漏掉了什么?据我所知,这个样例应用程序有使用多个作用域的多个提供者--但仍然是一个用户,它触发登录,从而通过Spring Security自动生成OAuth2令牌。(此问题还包括:Migrating from Spring Boot Oauth2 to Spring Security 5)[1]
- https://github.com/spring-cloud/spring-cloud-openfeign/issues/417-&>目前没有替代
OAuth2FeignRequestInterceptor
的
我的假设是我可以以某种方式使用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屋!