From a95f2a23d26978bea4d8e4077cd832834107d5c7 Mon Sep 17 00:00:00 2001 From: buildmaster Date: Tue, 6 Sep 2022 10:57:04 +0000 Subject: [PATCH 1/7] Bumping versions --- .../cloud/openfeign/support/SpringEncoder.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringEncoder.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringEncoder.java index 5dea96f3..f68933d5 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringEncoder.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringEncoder.java @@ -250,9 +250,9 @@ public class SpringEncoder implements Encoder { protected boolean binaryContentType(FeignOutputMessage outputMessage) { MediaType contentType = outputMessage.getHeaders().getContentType(); return contentType == null || Stream - .of(MediaType.APPLICATION_CBOR, MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_PDF, - MediaType.IMAGE_GIF, MediaType.IMAGE_JPEG, MediaType.IMAGE_PNG) - .anyMatch(mediaType -> mediaType.includes(contentType)); + .of(MediaType.APPLICATION_CBOR, MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_PDF, + MediaType.IMAGE_GIF, MediaType.IMAGE_JPEG, MediaType.IMAGE_PNG) + .anyMatch(mediaType -> mediaType.includes(contentType)); } private final class FeignOutputMessage implements HttpOutputMessage { From 9a8dae2cba8e3efcd42fed087c72ffb4483927f9 Mon Sep 17 00:00:00 2001 From: buildmaster Date: Tue, 6 Sep 2022 20:20:22 +0000 Subject: [PATCH 2/7] Update SNAPSHOT to 3.1.4 --- docs/pom.xml | 2 +- pom.xml | 6 +++--- spring-cloud-openfeign-core/pom.xml | 2 +- spring-cloud-openfeign-dependencies/pom.xml | 4 ++-- spring-cloud-starter-openfeign/pom.xml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/pom.xml b/docs/pom.xml index 79cabddb..4def7404 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-openfeign - 3.1.5-SNAPSHOT + 3.1.4 spring-cloud-openfeign-docs jar diff --git a/pom.xml b/pom.xml index ed1a3b8c..433be07c 100644 --- a/pom.xml +++ b/pom.xml @@ -4,14 +4,14 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 spring-cloud-openfeign - 3.1.5-SNAPSHOT + 3.1.4 pom Spring Cloud OpenFeign Spring Cloud OpenFeign org.springframework.cloud spring-cloud-build - 3.1.5-SNAPSHOT + 3.1.4 @@ -26,7 +26,7 @@ ${basedir} 2.11.3 - 3.1.5-SNAPSHOT + 3.1.4 2.10 diff --git a/spring-cloud-openfeign-core/pom.xml b/spring-cloud-openfeign-core/pom.xml index 863e314c..1a362f73 100644 --- a/spring-cloud-openfeign-core/pom.xml +++ b/spring-cloud-openfeign-core/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-openfeign - 3.1.5-SNAPSHOT + 3.1.4 .. spring-cloud-openfeign-core diff --git a/spring-cloud-openfeign-dependencies/pom.xml b/spring-cloud-openfeign-dependencies/pom.xml index 16605ed7..245485d7 100644 --- a/spring-cloud-openfeign-dependencies/pom.xml +++ b/spring-cloud-openfeign-dependencies/pom.xml @@ -6,11 +6,11 @@ spring-cloud-dependencies-parent org.springframework.cloud - 3.1.5-SNAPSHOT + 3.1.4 spring-cloud-openfeign-dependencies - 3.1.5-SNAPSHOT + 3.1.4 pom spring-cloud-openfeign-dependencies Spring Cloud OpenFeign Dependencies diff --git a/spring-cloud-starter-openfeign/pom.xml b/spring-cloud-starter-openfeign/pom.xml index 2a7bf0db..94c7c1c9 100644 --- a/spring-cloud-starter-openfeign/pom.xml +++ b/spring-cloud-starter-openfeign/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-openfeign - 3.1.5-SNAPSHOT + 3.1.4 .. spring-cloud-starter-openfeign From 11e9c5dab0b24cd325de285b7f07793dfb4e878b Mon Sep 17 00:00:00 2001 From: buildmaster Date: Tue, 6 Sep 2022 20:22:00 +0000 Subject: [PATCH 3/7] Going back to snapshots --- docs/pom.xml | 2 +- pom.xml | 6 +++--- spring-cloud-openfeign-core/pom.xml | 2 +- spring-cloud-openfeign-dependencies/pom.xml | 4 ++-- spring-cloud-starter-openfeign/pom.xml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/pom.xml b/docs/pom.xml index 4def7404..79cabddb 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-openfeign - 3.1.4 + 3.1.5-SNAPSHOT spring-cloud-openfeign-docs jar diff --git a/pom.xml b/pom.xml index 433be07c..ed1a3b8c 100644 --- a/pom.xml +++ b/pom.xml @@ -4,14 +4,14 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 spring-cloud-openfeign - 3.1.4 + 3.1.5-SNAPSHOT pom Spring Cloud OpenFeign Spring Cloud OpenFeign org.springframework.cloud spring-cloud-build - 3.1.4 + 3.1.5-SNAPSHOT @@ -26,7 +26,7 @@ ${basedir} 2.11.3 - 3.1.4 + 3.1.5-SNAPSHOT 2.10 diff --git a/spring-cloud-openfeign-core/pom.xml b/spring-cloud-openfeign-core/pom.xml index 1a362f73..863e314c 100644 --- a/spring-cloud-openfeign-core/pom.xml +++ b/spring-cloud-openfeign-core/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-openfeign - 3.1.4 + 3.1.5-SNAPSHOT .. spring-cloud-openfeign-core diff --git a/spring-cloud-openfeign-dependencies/pom.xml b/spring-cloud-openfeign-dependencies/pom.xml index 245485d7..16605ed7 100644 --- a/spring-cloud-openfeign-dependencies/pom.xml +++ b/spring-cloud-openfeign-dependencies/pom.xml @@ -6,11 +6,11 @@ spring-cloud-dependencies-parent org.springframework.cloud - 3.1.4 + 3.1.5-SNAPSHOT spring-cloud-openfeign-dependencies - 3.1.4 + 3.1.5-SNAPSHOT pom spring-cloud-openfeign-dependencies Spring Cloud OpenFeign Dependencies diff --git a/spring-cloud-starter-openfeign/pom.xml b/spring-cloud-starter-openfeign/pom.xml index 94c7c1c9..2a7bf0db 100644 --- a/spring-cloud-starter-openfeign/pom.xml +++ b/spring-cloud-starter-openfeign/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-openfeign - 3.1.4 + 3.1.5-SNAPSHOT .. spring-cloud-starter-openfeign From d501cbbd07fed010f933f9e9c111bc3c140cc476 Mon Sep 17 00:00:00 2001 From: dzcr <1137729123@qq.com> Date: Fri, 23 Sep 2022 17:28:16 +0800 Subject: [PATCH 4/7] step 1 --- spring-cloud-openfeign-core/pom.xml | 4 + .../openfeign/FeignAutoConfiguration.java | 21 ++- .../OAuth2AccessTokenInterceptor.java | 177 ++++++++++++++++++ .../FeignAutoConfigurationTests.java | 67 +++++-- .../OAuth2AccessTokenInterceptorTests.java | 170 +++++++++++++++++ spring-cloud-openfeign-dependencies/pom.xml | 6 + 6 files changed, 429 insertions(+), 16 deletions(-) create mode 100644 spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java create mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java diff --git a/spring-cloud-openfeign-core/pom.xml b/spring-cloud-openfeign-core/pom.xml index 863e314c..b441393d 100644 --- a/spring-cloud-openfeign-core/pom.xml +++ b/spring-cloud-openfeign-core/pom.xml @@ -225,6 +225,10 @@ 2.11.0 test + + org.springframework.security + spring-security-oauth2-client + diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java index 857f7f75..ec97c25d 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java @@ -51,6 +51,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.interceptor.CacheInterceptor; import org.springframework.cloud.client.actuator.HasFeatures; @@ -62,6 +63,7 @@ import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionMa import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory; import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory; import org.springframework.cloud.commons.httpclient.OkHttpClientFactory; +import org.springframework.cloud.openfeign.security.OAuth2AccessTokenInterceptor; import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor; import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorConfigurer; import org.springframework.cloud.openfeign.support.FeignEncoderProperties; @@ -74,7 +76,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; import org.springframework.data.domain.Sort; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; import static org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorBuilder.buildWithConfigurers; @@ -91,6 +95,7 @@ import static org.springframework.cloud.openfeign.security.OAuth2FeignRequestInt * @author Kwangyong Kim * @author Sam Kruglov * @author Wojciech Mąka + * @author Dangzhicairang(小水牛) */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Feign.class) @@ -369,12 +374,24 @@ public class FeignAutoConfiguration { @Bean @ConditionalOnMissingBean(OAuth2FeignRequestInterceptor.class) - @ConditionalOnBean({ OAuth2ClientContext.class, OAuth2ProtectedResourceDetails.class }) + @ConditionalOnBean({OAuth2ClientContext.class, OAuth2ProtectedResourceDetails.class}) public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext, - OAuth2ProtectedResourceDetails resource, List configurers) { + OAuth2ProtectedResourceDetails resource, List configurers) { return buildWithConfigurers(oAuth2ClientContext, resource, configurers); } + @Bean + @ConditionalOnBean({OAuth2AuthorizedClientService.class, ClientRegistrationRepository.class}) + public OAuth2AccessTokenInterceptor defaultOAuth2AccessTokenInterceptor( + @Value("${spring.cloud.openfeign.oauth2.specifiedClientIds:}") List specifiedClientIds, + OAuth2ClientProperties oAuth2ClientProperties, + OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + ClientRegistrationRepository clientRegistrationRepository) { + + return new OAuth2AccessTokenInterceptor(specifiedClientIds, oAuth2ClientProperties, + oAuth2AuthorizedClientService, clientRegistrationRepository); + } + } } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java new file mode 100644 index 00000000..cc017006 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java @@ -0,0 +1,177 @@ +/* + * Copyright 2015-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.openfeign.security; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import feign.RequestInterceptor; +import feign.RequestTemplate; + +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.util.StringUtils; + +/** + * RequestInterceptor for OAuth2 Feign Requests. By default, It uses the + * {@link AuthorizedClientServiceOAuth2AuthorizedClientManager } to get + * {@link OAuth2AuthorizedClient } that hold an {@link OAuth2AccessToken }. Use the + * Client(s) from properties if not specific the field + * {@link OAuth2AccessTokenInterceptor#specifiedClientIds} + * + * @author Dangzhicairang(小水牛) + * @since 4.0.0 + */ +public class OAuth2AccessTokenInterceptor implements RequestInterceptor { + + /** + * The name of the token. + */ + public static final String BEARER = "Bearer"; + + /** + * The name of the header. + */ + public static final String AUTHORIZATION = "Authorization"; + + private final String tokenType; + + private final String header; + + private final List specifiedClientIds; + + private final OAuth2ClientProperties oAuth2ClientProperties; + + private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService; + + private OAuth2AuthorizedClientManager authorizedClientManager; + + public void setAuthorizedClientManager(OAuth2AuthorizedClientManager authorizedClientManager) { + this.authorizedClientManager = authorizedClientManager; + } + + private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken("anonymous", + "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + + public OAuth2AccessTokenInterceptor(OAuth2ClientProperties oAuth2ClientProperties, + OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + ClientRegistrationRepository clientRegistrationRepository) { + this(new ArrayList<>(), oAuth2ClientProperties, oAuth2AuthorizedClientService, clientRegistrationRepository); + } + + public OAuth2AccessTokenInterceptor(List specifiedClientIds, OAuth2ClientProperties oAuth2ClientProperties, + OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + ClientRegistrationRepository clientRegistrationRepository) { + this(BEARER, AUTHORIZATION, specifiedClientIds, oAuth2ClientProperties, oAuth2AuthorizedClientService, + clientRegistrationRepository); + } + + public OAuth2AccessTokenInterceptor(String tokenType, String header, List specifiedClientIds, + OAuth2ClientProperties oAuth2ClientProperties, OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + ClientRegistrationRepository clientRegistrationRepository) { + this.tokenType = tokenType; + this.header = header; + this.specifiedClientIds = specifiedClientIds; + this.oAuth2ClientProperties = oAuth2ClientProperties; + this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService; + this.authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager( + clientRegistrationRepository, this.oAuth2AuthorizedClientService); + } + + @Override + public void apply(RequestTemplate template) { + template.header(header); + template.header(header, extract(tokenType)); + } + + protected String extract(String tokenType) { + OAuth2AccessToken accessToken = getToken(); + return String.format("%s %s", tokenType, accessToken.getTokenValue()); + } + + public OAuth2AccessToken getToken() { + + // if specific, try to use them to get token. + for (String clientId : this.specifiedClientIds) { + OAuth2AccessToken token = this.getToken(clientId); + if (token != null) { + return token; + } + } + + // use clients from properties by default + for (String clientId : Optional.ofNullable(this.oAuth2ClientProperties) + .map(OAuth2ClientProperties::getRegistration).map(Map::keySet) + .orElse(new HashSet<>())) { + OAuth2AccessToken token = this.getToken(clientId); + if (token != null) { + return token; + } + } + + throw new IllegalStateException("No token acquired, which is illegal according to the contract."); + } + + protected OAuth2AccessToken getToken(String clientId) { + + if (!StringUtils.hasText(clientId)) { + return null; + } + + Authentication principal = SecurityContextHolder.getContext().getAuthentication(); + if (principal == null) { + principal = ANONYMOUS_AUTHENTICATION; + } + + // already exist + OAuth2AuthorizedClient oAuth2AuthorizedClient = oAuth2AuthorizedClientService.loadAuthorizedClient(clientId, + principal.getName()); + if (oAuth2AuthorizedClient != null) { + OAuth2AccessToken accessToken = oAuth2AuthorizedClient.getAccessToken(); + if (accessToken != null && this.noExpire(accessToken)) { + return accessToken; + } + } + + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientId) + .principal(principal).build(); + OAuth2AuthorizedClient authorize = this.authorizedClientManager.authorize(authorizeRequest); + return Optional.ofNullable(authorize).map(OAuth2AuthorizedClient::getAccessToken) + .filter(this::noExpire) + .orElse(null); + } + + protected boolean noExpire(OAuth2AccessToken token) { + return Optional.ofNullable(token).map(OAuth2AccessToken::getExpiresAt) + .map(expire -> expire.isAfter(Instant.now())).orElse(false); + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java index b906ff55..4997f29e 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java @@ -17,6 +17,8 @@ package org.springframework.cloud.openfeign; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; import feign.Target; import org.assertj.core.api.Condition; @@ -29,12 +31,15 @@ import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor; import org.springframework.cloud.openfeign.FeignAutoConfiguration.CircuitBreakerPresentFeignTargeterConfiguration.DefaultCircuitBreakerNameResolver; import org.springframework.cloud.openfeign.security.MockOAuth2ClientContext; +import org.springframework.cloud.openfeign.security.OAuth2AccessTokenInterceptor; import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor; import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorBuilder; import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorConfigurer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails; import static org.assertj.core.api.Assertions.assertThat; @@ -46,6 +51,7 @@ import static org.mockito.Mockito.mock; * @author Andrii Bohutskyi * @author Kwangyong Kim * @author Wojciech Mąka + * @author Dangzhicairang(小水牛) */ class FeignAutoConfigurationTests { @@ -123,12 +129,30 @@ class FeignAutoConfigurationTests { @Test void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithCustomAccessTokenProviderInterceptor() { - runner.withPropertyValues("feign.oauth2.enabled=true").withBean(MockOAuth2ClientContext.class, "token") - .withBean(BaseOAuth2ProtectedResourceDetails.class) - .withBean(CustomOAuth2FeignRequestInterceptorConfigurer.class).run(ctx -> { - assertOauth2FeignRequestInterceptorExists(ctx); - assertAccessTokenProviderInterceptorExists(ctx, BasicAuthenticationInterceptor.class); - }); + runner.withPropertyValues("feign.oauth2.enabled=true") + .withBean(MockOAuth2ClientContext.class, "token") + .withBean(BaseOAuth2ProtectedResourceDetails.class) + .withBean(CustomOAuth2FeignRequestInterceptorConfigurer.class).run(ctx -> { + assertOauth2FeignRequestInterceptorExists(ctx); + assertAccessTokenProviderInterceptorExists(ctx, BasicAuthenticationInterceptor.class); + }); + } + + @Test + void shouldInstantiateFeignOAuth2FeignRequestInterceptor() { + runner.withPropertyValues("spring.cloud.openfeign.oauth2.enabled=true", + "spring.cloud.openfeign.oauth2.specifiedClientIds=feign-client") + .withBean(OAuth2AuthorizedClientService.class, () -> mock(OAuth2AuthorizedClientService.class)) + .withBean(ClientRegistrationRepository.class, () -> mock(ClientRegistrationRepository.class)) + .run(ctx -> { + assertOauth2AccessTokenInterceptorExists(ctx); + assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue(ctx, + new ArrayList() { + { + add("feign-client"); + } + }); + }); } private void assertOauth2FeignRequestInterceptorExists(ConfigurableApplicationContext ctx) { @@ -137,28 +161,43 @@ class FeignAutoConfigurationTests { } private void assertAccessTokenProviderInterceptorExists(ConfigurableApplicationContext ctx, - Class clazz) { + Class clazz) { AssertableApplicationContext context = AssertableApplicationContext.get(() -> ctx); - assertThat(context).getBean(OAuth2FeignRequestInterceptor.class).extracting("accessTokenProvider") + assertThat(context).getBean(OAuth2FeignRequestInterceptor.class) + .extracting("accessTokenProvider") .extracting("interceptors").asList().first().isInstanceOf(clazz); } private void assertAccessTokenProviderInterceptorNotExists(ConfigurableApplicationContext ctx, - Class clazz) { + Class clazz) { AssertableApplicationContext context = AssertableApplicationContext.get(() -> ctx); - assertThat(context).getBean(OAuth2FeignRequestInterceptor.class).extracting("accessTokenProvider") - .extracting("interceptors").asList().filteredOn(obj -> clazz.isAssignableFrom(obj.getClass())) - .isEmpty(); + assertThat(context).getBean(OAuth2FeignRequestInterceptor.class) + .extracting("accessTokenProvider") + .extracting("interceptors").asList() + .filteredOn(obj -> clazz.isAssignableFrom(obj.getClass())) + .isEmpty(); + } + + private void assertOauth2AccessTokenInterceptorExists(ConfigurableApplicationContext ctx) { + AssertableApplicationContext context = AssertableApplicationContext.get(() -> ctx); + assertThat(context).hasSingleBean(OAuth2AccessTokenInterceptor.class); + } + + private void assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue( + ConfigurableApplicationContext ctx, List expectedValue) { + final OAuth2AccessTokenInterceptor bean = ctx.getBean(OAuth2AccessTokenInterceptor.class); + assertThat(bean).hasFieldOrPropertyWithValue("specifiedClientIds", expectedValue); } private void assertOnlyOneTargeterPresent(ConfigurableApplicationContext ctx, Class beanClass) { - assertThat(ctx.getBeansOfType(Targeter.class)).hasSize(1).hasValueSatisfying(new Condition<>( + assertThat(ctx.getBeansOfType(Targeter.class)).hasSize(1) + .hasValueSatisfying(new Condition<>( beanClass::isInstance, String.format("Targeter should be an instance of %s", beanClass))); } private void assertThatFeignCircuitBreakerTargeterHasGroupEnabledPropertyWithValue( - ConfigurableApplicationContext ctx, boolean expectedValue) { + ConfigurableApplicationContext ctx, boolean expectedValue) { final FeignCircuitBreakerTargeter bean = ctx.getBean(FeignCircuitBreakerTargeter.class); assertThat(bean).hasFieldOrPropertyWithValue("circuitBreakerGroupEnabled", expectedValue); } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java new file mode 100644 index 00000000..4242609b --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java @@ -0,0 +1,170 @@ +/* + * Copyright 2015-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.openfeign.security; + +import java.time.Instant; +import java.util.HashMap; + +import feign.Request.HttpMethod; +import feign.RequestTemplate; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.util.AlternativeJdkIdGenerator; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Dangzhicairang(小水牛) + */ +class OAuth2AccessTokenInterceptorTests { + + private OAuth2AccessTokenInterceptor oAuth2AccessTokenInterceptor; + + private RequestTemplate requestTemplate; + + private OAuth2ClientProperties mockOAuth2ClientProperties; + + private static final String DEFAULT_CLIENT_ID = "feign-client"; + + @BeforeEach + void setUp() { + + requestTemplate = new RequestTemplate().method(HttpMethod.GET); + + mockOAuth2ClientProperties = mock(OAuth2ClientProperties.class); + given(mockOAuth2ClientProperties.getRegistration()) + .willReturn(new HashMap() { + { + put(DEFAULT_CLIENT_ID, mock(OAuth2ClientProperties.Registration.class)); + } + }); + + } + + @Test + void noTokenAcquired() { + + OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); + given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(null); + + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, + mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); + + OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock(OAuth2AuthorizedClientManager.class); + given(mockOAuth2AuthorizedClientManager.authorize(any())).willReturn(null); + + oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); + + Assertions.assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate)) + .withMessage("No token acquired, which is illegal according to the contract."); + + } + + @Test + void validTokenAcquired() { + + OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); + given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(null); + + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, + mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); + + OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock(OAuth2AuthorizedClientManager.class); + given(mockOAuth2AuthorizedClientManager.authorize(any())).willReturn(validTokenOAuth2AuthorizedClient()); + + oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); + + oAuth2AccessTokenInterceptor.apply(requestTemplate); + + Assertions.assertThat(requestTemplate.headers().get("Authorization")) + .contains("Bearer Valid Token"); + } + + @Test + void expireTokenAcquired() { + + OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); + given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(null); + + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, + mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); + + OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock(OAuth2AuthorizedClientManager.class); + given(mockOAuth2AuthorizedClientManager.authorize(any())).willReturn(expiredTokenOAuth2AuthorizedClient()); + + oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); + + Assertions.assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate)) + .withMessage("No token acquired, which is illegal according to the contract."); + } + + @Test + void acquireTokenFromAuthorizedClient() { + OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); + given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())) + .willReturn(validTokenOAuth2AuthorizedClient()); + + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, + mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); + + oAuth2AccessTokenInterceptor.apply(requestTemplate); + + Assertions.assertThat(requestTemplate.headers().get("Authorization")) + .contains("Bearer Valid Token"); + } + + private OAuth2AccessToken validToken() { + return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "Valid Token", Instant.now(), + Instant.now().plusSeconds(60L)); + } + + private OAuth2AccessToken expiredToken() { + return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "Expired Token", + Instant.now().minusSeconds(61L), Instant.now().minusSeconds(60L)); + } + + private OAuth2AuthorizedClient validTokenOAuth2AuthorizedClient() { + return new OAuth2AuthorizedClient(defaultClientRegistration(), "anonymousUser", validToken()); + } + + private OAuth2AuthorizedClient expiredTokenOAuth2AuthorizedClient() { + return new OAuth2AuthorizedClient(defaultClientRegistration(), "anonymousUser", expiredToken()); + } + + private ClientRegistration defaultClientRegistration() { + return ClientRegistration.withRegistrationId(new AlternativeJdkIdGenerator().generateId() + .toString()) + .clientId(DEFAULT_CLIENT_ID).tokenUri("mock token uri") + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).build(); + } + +} diff --git a/spring-cloud-openfeign-dependencies/pom.xml b/spring-cloud-openfeign-dependencies/pom.xml index 16605ed7..a46020b6 100644 --- a/spring-cloud-openfeign-dependencies/pom.xml +++ b/spring-cloud-openfeign-dependencies/pom.xml @@ -19,6 +19,7 @@ 3.8.0 2.5.2 + 6.0.0-SNAPSHOT @@ -27,6 +28,11 @@ spring-security-oauth2-autoconfigure ${spring-security-oauth2-autoconfigure.version} + + org.springframework.security + spring-security-oauth2-client + ${spring-security-oauth2-client.version} + org.springframework.cloud spring-cloud-openfeign-core From 60fc4a53e7ea1f2846afeb27e37723e4adf8eee1 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Tue, 27 Sep 2022 15:14:54 +0200 Subject: [PATCH 5/7] Refactor. Add docs. Fix dependencies. --- .../main/asciidoc/spring-cloud-openfeign.adoc | 13 ++ spring-cloud-openfeign-core/pom.xml | 1 + .../openfeign/FeignAutoConfiguration.java | 9 +- .../OAuth2AccessTokenInterceptor.java | 116 +++++++-------- ...itional-spring-configuration-metadata.json | 6 + .../FeignAutoConfigurationTests.java | 15 +- .../OAuth2AccessTokenInterceptorTests.java | 140 +++++++++--------- spring-cloud-openfeign-dependencies/pom.xml | 2 - 8 files changed, 150 insertions(+), 152 deletions(-) diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index 26cbe520..3dd8e88e 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -811,6 +811,19 @@ When the flag is set to true, and the oauth2 client context resource details are Sometimes, when load balancing is enabled for Feign clients, you may want to use load balancing for fetching access tokens, too. To do so, you should ensure that the load balancer is on the classpath (spring-cloud-starter-loadbalancer) and explicitly enable load balancing for OAuth2FeignRequestInterceptor by setting the following flag: ---- feign.oauth2.load-balanced=true + +When the flag is set to true, and the oauth2 client context resource details are present, a bean of class `OAuth2AccessTokenInterceptor` is created. Before each request, the interceptor resolves the required access token and includes it as a header. +`OAuth2AccessTokenInterceptor` uses the `AuthorizedClientServiceOAuth2AuthorizedClientManager` to get `OAuth2AuthorizedClient` that holds an `OAuth2AccessToken`. If the user has specified an OAuth2 `clientId` using the `spring.cloud.openfeign.oauth2.clientId` property, it will be used to retrieve the token. If the token is not retrieved or the `clientId` has not been specified, the `serviceId` retrieved from the `url` host segment will be used. + +TIP:: Using the `serviceId` as OAuth2 client id is convenient for load-balanced Feign clients. For non-load-balanced ones, the property-based `clientId` is a suitable approach. + +=== Transform the load-balanced HTTP request + +You can use the selected `ServiceInstance` to transform the load-balanced HTTP Request. + +For `Request`, you need to implement and define `LoadBalancerFeignRequestTransformer`, as follows: + +[source,java,indent=0] ---- == Configuration properties diff --git a/spring-cloud-openfeign-core/pom.xml b/spring-cloud-openfeign-core/pom.xml index b441393d..306c4474 100644 --- a/spring-cloud-openfeign-core/pom.xml +++ b/spring-cloud-openfeign-core/pom.xml @@ -228,6 +228,7 @@ org.springframework.security spring-security-oauth2-client + true diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java index ec97c25d..a182c0d9 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java @@ -51,7 +51,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.interceptor.CacheInterceptor; import org.springframework.cloud.client.actuator.HasFeatures; @@ -383,13 +382,11 @@ public class FeignAutoConfiguration { @Bean @ConditionalOnBean({OAuth2AuthorizedClientService.class, ClientRegistrationRepository.class}) public OAuth2AccessTokenInterceptor defaultOAuth2AccessTokenInterceptor( - @Value("${spring.cloud.openfeign.oauth2.specifiedClientIds:}") List specifiedClientIds, - OAuth2ClientProperties oAuth2ClientProperties, + @Value("${spring.cloud.openfeign.oauth2.clientId:}") String clientId, OAuth2AuthorizedClientService oAuth2AuthorizedClientService, ClientRegistrationRepository clientRegistrationRepository) { - - return new OAuth2AccessTokenInterceptor(specifiedClientIds, oAuth2ClientProperties, - oAuth2AuthorizedClientService, clientRegistrationRepository); + return new OAuth2AccessTokenInterceptor(clientId, oAuth2AuthorizedClientService, + clientRegistrationRepository); } } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java index cc017006..b421d0ae 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java @@ -16,17 +16,14 @@ package org.springframework.cloud.openfeign.security; +import java.net.URI; import java.time.Instant; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; import java.util.Optional; import feign.RequestInterceptor; import feign.RequestTemplate; +import feign.Target; -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; @@ -38,16 +35,22 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** - * RequestInterceptor for OAuth2 Feign Requests. By default, It uses the + * A {@link RequestInterceptor} for OAuth2 Feign Requests. By default, it uses the * {@link AuthorizedClientServiceOAuth2AuthorizedClientManager } to get - * {@link OAuth2AuthorizedClient } that hold an {@link OAuth2AccessToken }. Use the - * Client(s) from properties if not specific the field - * {@link OAuth2AccessTokenInterceptor#specifiedClientIds} + * {@link OAuth2AuthorizedClient } that holds an {@link OAuth2AccessToken }. If the user + * has specified an OAuth2 {@code clientId} using the + * {@code spring.cloud.openfeign.oauth2.clientId} property, it will be used to retrieve + * the token. If the token is not retrieved or the {@code clientId} has not been + * specified, the {@code serviceId} retrieved from the {@code url} host segment will be + * used. This approach is convenient for load-balanced Feign clients. For + * non-load-balanced ones, the property-based {@code clientId} is a suitable approach. * * @author Dangzhicairang(小水牛) + * @author Olga Maciaszek-Sharma * @since 4.0.0 */ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { @@ -66,9 +69,7 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { private final String header; - private final List specifiedClientIds; - - private final OAuth2ClientProperties oAuth2ClientProperties; + private final String clientId; private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService; @@ -79,69 +80,56 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { } private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken("anonymous", - "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); - public OAuth2AccessTokenInterceptor(OAuth2ClientProperties oAuth2ClientProperties, - OAuth2AuthorizedClientService oAuth2AuthorizedClientService, - ClientRegistrationRepository clientRegistrationRepository) { - this(new ArrayList<>(), oAuth2ClientProperties, oAuth2AuthorizedClientService, clientRegistrationRepository); + public OAuth2AccessTokenInterceptor(OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + ClientRegistrationRepository clientRegistrationRepository) { + this(null, oAuth2AuthorizedClientService, clientRegistrationRepository); } - public OAuth2AccessTokenInterceptor(List specifiedClientIds, OAuth2ClientProperties oAuth2ClientProperties, - OAuth2AuthorizedClientService oAuth2AuthorizedClientService, - ClientRegistrationRepository clientRegistrationRepository) { - this(BEARER, AUTHORIZATION, specifiedClientIds, oAuth2ClientProperties, oAuth2AuthorizedClientService, - clientRegistrationRepository); + public OAuth2AccessTokenInterceptor(String clientId, OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + ClientRegistrationRepository clientRegistrationRepository) { + this(BEARER, AUTHORIZATION, clientId, oAuth2AuthorizedClientService, clientRegistrationRepository); } - public OAuth2AccessTokenInterceptor(String tokenType, String header, List specifiedClientIds, - OAuth2ClientProperties oAuth2ClientProperties, OAuth2AuthorizedClientService oAuth2AuthorizedClientService, - ClientRegistrationRepository clientRegistrationRepository) { + public OAuth2AccessTokenInterceptor(String tokenType, String header, String clientId, + OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + ClientRegistrationRepository clientRegistrationRepository) { this.tokenType = tokenType; this.header = header; - this.specifiedClientIds = specifiedClientIds; - this.oAuth2ClientProperties = oAuth2ClientProperties; + this.clientId = clientId; this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService; this.authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager( - clientRegistrationRepository, this.oAuth2AuthorizedClientService); + clientRegistrationRepository, this.oAuth2AuthorizedClientService); } @Override public void apply(RequestTemplate template) { + OAuth2AccessToken token = getToken(template); + String extractedToken = String.format("%s %s", tokenType, token.getTokenValue()); template.header(header); - template.header(header, extract(tokenType)); + template.header(header, extractedToken); } - protected String extract(String tokenType) { - OAuth2AccessToken accessToken = getToken(); - return String.format("%s %s", tokenType, accessToken.getTokenValue()); - } - - public OAuth2AccessToken getToken() { - - // if specific, try to use them to get token. - for (String clientId : this.specifiedClientIds) { - OAuth2AccessToken token = this.getToken(clientId); + public OAuth2AccessToken getToken(RequestTemplate template) { + // If specified, try to use them to get token. + if (StringUtils.hasText(clientId)) { + OAuth2AccessToken token = getToken(clientId); if (token != null) { return token; } } - // use clients from properties by default - for (String clientId : Optional.ofNullable(this.oAuth2ClientProperties) - .map(OAuth2ClientProperties::getRegistration).map(Map::keySet) - .orElse(new HashSet<>())) { - OAuth2AccessToken token = this.getToken(clientId); - if (token != null) { - return token; - } + // If not specified use host (synonymous with serviceId for load-balanced + // requests; non-load-balanced requests should use the method above). + OAuth2AccessToken token = getToken(getServiceId(template)); + if (token != null) { + return token; } - - throw new IllegalStateException("No token acquired, which is illegal according to the contract."); + throw new IllegalStateException("OAuth2 token has not been successfully acquired."); } protected OAuth2AccessToken getToken(String clientId) { - if (!StringUtils.hasText(clientId)) { return null; } @@ -151,27 +139,35 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { principal = ANONYMOUS_AUTHENTICATION; } - // already exist + // Already exist OAuth2AuthorizedClient oAuth2AuthorizedClient = oAuth2AuthorizedClientService.loadAuthorizedClient(clientId, - principal.getName()); + principal.getName()); if (oAuth2AuthorizedClient != null) { OAuth2AccessToken accessToken = oAuth2AuthorizedClient.getAccessToken(); - if (accessToken != null && this.noExpire(accessToken)) { + if (accessToken != null && notExpired(accessToken)) { return accessToken; } } OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientId) - .principal(principal).build(); - OAuth2AuthorizedClient authorize = this.authorizedClientManager.authorize(authorizeRequest); - return Optional.ofNullable(authorize).map(OAuth2AuthorizedClient::getAccessToken) - .filter(this::noExpire) - .orElse(null); + .principal(principal).build(); + OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest); + return Optional.ofNullable(authorizedClient).map(OAuth2AuthorizedClient::getAccessToken) + .filter(this::notExpired).orElse(null); } - protected boolean noExpire(OAuth2AccessToken token) { + protected boolean notExpired(OAuth2AccessToken token) { return Optional.ofNullable(token).map(OAuth2AccessToken::getExpiresAt) - .map(expire -> expire.isAfter(Instant.now())).orElse(false); + .map(expire -> expire.isAfter(Instant.now())).orElse(false); + } + + private static String getServiceId(RequestTemplate template) { + Target feignTarget = template.feignTarget(); + Assert.notNull(feignTarget, "feignTarget may not be null"); + String url = feignTarget.url(); + Assert.hasLength(url, "url may not be empty"); + final URI originalUri = URI.create(url); + return originalUri.getHost(); } } diff --git a/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 73ff218b..88c42c56 100644 --- a/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -79,6 +79,12 @@ "type": "java.lang.Boolean", "description": "Enables load balancing for oauth2 access token provider.", "defaultValue": "false" + }, + { + "name": "feign.oauth2.registrationClientId", + "type": "java.lang.String", + "description": "Provides a clientId to be used with OAuth2.", + "defaultValue": "" } ] } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java index 4997f29e..a8f1c919 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java @@ -17,8 +17,6 @@ package org.springframework.cloud.openfeign; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; import feign.Target; import org.assertj.core.api.Condition; @@ -141,17 +139,12 @@ class FeignAutoConfigurationTests { @Test void shouldInstantiateFeignOAuth2FeignRequestInterceptor() { runner.withPropertyValues("spring.cloud.openfeign.oauth2.enabled=true", - "spring.cloud.openfeign.oauth2.specifiedClientIds=feign-client") + "spring.cloud.openfeign.oauth2.clientId=feign-client") .withBean(OAuth2AuthorizedClientService.class, () -> mock(OAuth2AuthorizedClientService.class)) .withBean(ClientRegistrationRepository.class, () -> mock(ClientRegistrationRepository.class)) .run(ctx -> { assertOauth2AccessTokenInterceptorExists(ctx); - assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue(ctx, - new ArrayList() { - { - add("feign-client"); - } - }); + assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue(ctx, "feign-client"); }); } @@ -184,9 +177,9 @@ class FeignAutoConfigurationTests { } private void assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue( - ConfigurableApplicationContext ctx, List expectedValue) { + ConfigurableApplicationContext ctx, String expectedValue) { final OAuth2AccessTokenInterceptor bean = ctx.getBean(OAuth2AccessTokenInterceptor.class); - assertThat(bean).hasFieldOrPropertyWithValue("specifiedClientIds", expectedValue); + assertThat(bean).hasFieldOrPropertyWithValue("clientId", expectedValue); } private void assertOnlyOneTargeterPresent(ConfigurableApplicationContext ctx, Class beanClass) { diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java index 4242609b..9a825e57 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java @@ -17,15 +17,14 @@ package org.springframework.cloud.openfeign.security; import java.time.Instant; -import java.util.HashMap; import feign.Request.HttpMethod; import feign.RequestTemplate; -import org.assertj.core.api.Assertions; +import feign.Target; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; @@ -35,121 +34,117 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.util.AlternativeJdkIdGenerator; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** + * Tests for {@link OAuth2AccessTokenInterceptor}. + * * @author Dangzhicairang(小水牛) + * @author Olga Maciaszek-Sharma + * */ class OAuth2AccessTokenInterceptorTests { + private final OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock( + OAuth2AuthorizedClientService.class); + + private final OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock( + OAuth2AuthorizedClientManager.class); + private OAuth2AccessTokenInterceptor oAuth2AccessTokenInterceptor; private RequestTemplate requestTemplate; - private OAuth2ClientProperties mockOAuth2ClientProperties; - private static final String DEFAULT_CLIENT_ID = "feign-client"; @BeforeEach void setUp() { - requestTemplate = new RequestTemplate().method(HttpMethod.GET); - - mockOAuth2ClientProperties = mock(OAuth2ClientProperties.class); - given(mockOAuth2ClientProperties.getRegistration()) - .willReturn(new HashMap() { - { - put(DEFAULT_CLIENT_ID, mock(OAuth2ClientProperties.Registration.class)); - } - }); - + Target feignTarget = mock(Target.class); + when(feignTarget.url()).thenReturn("http://test"); + requestTemplate.feignTarget(feignTarget); } @Test - void noTokenAcquired() { - - OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); - given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(null); - - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, - mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); - - OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock(OAuth2AuthorizedClientManager.class); - given(mockOAuth2AuthorizedClientManager.authorize(any())).willReturn(null); - + void shouldThrowExceptionWhenNoTokenAcquired() { + when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).thenReturn(null); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, + mock(ClientRegistrationRepository.class)); + when(mockOAuth2AuthorizedClientManager.authorize(any())).thenReturn(null); oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); - Assertions.assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate)) - .withMessage("No token acquired, which is illegal according to the contract."); - + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate)) + .withMessage("OAuth2 token has not been successfully acquired."); } @Test - void validTokenAcquired() { - - OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); - given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(null); - - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, - mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); - - OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock(OAuth2AuthorizedClientManager.class); - given(mockOAuth2AuthorizedClientManager.authorize(any())).willReturn(validTokenOAuth2AuthorizedClient()); - + void shouldAcquireValidToken() { + when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).thenReturn(null); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, + mock(ClientRegistrationRepository.class)); + when(mockOAuth2AuthorizedClientManager.authorize( + argThat((OAuth2AuthorizeRequest request) -> ("test").equals(request.getClientRegistrationId())))) + .thenReturn(validTokenOAuth2AuthorizedClient()); oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); oAuth2AccessTokenInterceptor.apply(requestTemplate); - Assertions.assertThat(requestTemplate.headers().get("Authorization")) - .contains("Bearer Valid Token"); + assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); } @Test - void expireTokenAcquired() { - - OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); - given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).willReturn(null); - - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, - mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); - - OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock(OAuth2AuthorizedClientManager.class); - given(mockOAuth2AuthorizedClientManager.authorize(any())).willReturn(expiredTokenOAuth2AuthorizedClient()); - + void shouldThrowExceptionWhenExpiredTokenAcquired() { + when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).thenReturn(null); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, + mock(ClientRegistrationRepository.class)); + when(mockOAuth2AuthorizedClientManager.authorize(any())).thenReturn(expiredTokenOAuth2AuthorizedClient()); oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); - Assertions.assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate)) - .withMessage("No token acquired, which is illegal according to the contract."); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate)) + .withMessage("OAuth2 token has not been successfully acquired."); } @Test - void acquireTokenFromAuthorizedClient() { - OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock(OAuth2AuthorizedClientService.class); - given(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())) - .willReturn(validTokenOAuth2AuthorizedClient()); - - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2ClientProperties, - mockOAuth2AuthorizedClientService, mock(ClientRegistrationRepository.class)); + void shouldAcquireTokenFromAuthorizedClient() { + when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(eq("test"), anyString())) + .thenReturn(validTokenOAuth2AuthorizedClient()); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, + mock(ClientRegistrationRepository.class)); oAuth2AccessTokenInterceptor.apply(requestTemplate); - Assertions.assertThat(requestTemplate.headers().get("Authorization")) - .contains("Bearer Valid Token"); + assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); + } + + @Test + void shouldAcquireValidTokenFromSpecifiedClientId() { + when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(eq("testId"), anyString())) + .thenReturn(validTokenOAuth2AuthorizedClient()); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor("testId", mockOAuth2AuthorizedClientService, + mock(ClientRegistrationRepository.class)); + + oAuth2AccessTokenInterceptor.apply(requestTemplate); + + assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); } private OAuth2AccessToken validToken() { return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "Valid Token", Instant.now(), - Instant.now().plusSeconds(60L)); + Instant.now().plusSeconds(60L)); } private OAuth2AccessToken expiredToken() { return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "Expired Token", - Instant.now().minusSeconds(61L), Instant.now().minusSeconds(60L)); + Instant.now().minusSeconds(61L), Instant.now().minusSeconds(60L)); } private OAuth2AuthorizedClient validTokenOAuth2AuthorizedClient() { @@ -161,10 +156,9 @@ class OAuth2AccessTokenInterceptorTests { } private ClientRegistration defaultClientRegistration() { - return ClientRegistration.withRegistrationId(new AlternativeJdkIdGenerator().generateId() - .toString()) - .clientId(DEFAULT_CLIENT_ID).tokenUri("mock token uri") - .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).build(); + return ClientRegistration.withRegistrationId(new AlternativeJdkIdGenerator().generateId().toString()) + .clientId(DEFAULT_CLIENT_ID).tokenUri("mock token uri") + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).build(); } } diff --git a/spring-cloud-openfeign-dependencies/pom.xml b/spring-cloud-openfeign-dependencies/pom.xml index a46020b6..70929ac7 100644 --- a/spring-cloud-openfeign-dependencies/pom.xml +++ b/spring-cloud-openfeign-dependencies/pom.xml @@ -19,7 +19,6 @@ 3.8.0 2.5.2 - 6.0.0-SNAPSHOT @@ -31,7 +30,6 @@ org.springframework.security spring-security-oauth2-client - ${spring-security-oauth2-client.version} org.springframework.cloud From afbe0b232b834f99b79fb9a00fdb9b313d79ec86 Mon Sep 17 00:00:00 2001 From: dzcr <1137729123@qq.com> Date: Fri, 30 Sep 2022 11:13:43 +0800 Subject: [PATCH 6/7] Adjust based on review. --- .../main/asciidoc/spring-cloud-openfeign.adoc | 4 +- .../openfeign/FeignAutoConfiguration.java | 13 ++-- .../OAuth2AccessTokenInterceptor.java | 45 ++++------- .../FeignAutoConfigurationTests.java | 23 +++--- .../OAuth2AccessTokenInterceptorTests.java | 75 +++++++------------ 5 files changed, 59 insertions(+), 101 deletions(-) diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index 3dd8e88e..2c893da4 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -813,9 +813,9 @@ Sometimes, when load balancing is enabled for Feign clients, you may want to use feign.oauth2.load-balanced=true When the flag is set to true, and the oauth2 client context resource details are present, a bean of class `OAuth2AccessTokenInterceptor` is created. Before each request, the interceptor resolves the required access token and includes it as a header. -`OAuth2AccessTokenInterceptor` uses the `AuthorizedClientServiceOAuth2AuthorizedClientManager` to get `OAuth2AuthorizedClient` that holds an `OAuth2AccessToken`. If the user has specified an OAuth2 `clientId` using the `spring.cloud.openfeign.oauth2.clientId` property, it will be used to retrieve the token. If the token is not retrieved or the `clientId` has not been specified, the `serviceId` retrieved from the `url` host segment will be used. +`OAuth2AccessTokenInterceptor` uses the `AuthorizedClientServiceOAuth2AuthorizedClientManager` to get `OAuth2AuthorizedClient` that holds an `OAuth2AccessToken`. If the user has specified an OAuth2 `clientRegistrationId` using the `spring.cloud.openfeign.oauth2.clientRegistrationId` property, it will be used to retrieve the token. If the token is not retrieved or the `clientRegistrationId` has not been specified, the `serviceId` retrieved from the `url` host segment will be used. -TIP:: Using the `serviceId` as OAuth2 client id is convenient for load-balanced Feign clients. For non-load-balanced ones, the property-based `clientId` is a suitable approach. +TIP:: Using the `serviceId` as OAuth2 client registrationId is convenient for load-balanced Feign clients. For non-load-balanced ones, the property-based `clientRegistrationId` is a suitable approach. === Transform the load-balanced HTTP request diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java index a182c0d9..2bec6315 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java @@ -75,6 +75,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; import org.springframework.data.domain.Sort; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2ClientContext; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; @@ -380,13 +381,13 @@ public class FeignAutoConfiguration { } @Bean - @ConditionalOnBean({OAuth2AuthorizedClientService.class, ClientRegistrationRepository.class}) + @ConditionalOnBean({ OAuth2AuthorizedClientService.class, ClientRegistrationRepository.class }) public OAuth2AccessTokenInterceptor defaultOAuth2AccessTokenInterceptor( - @Value("${spring.cloud.openfeign.oauth2.clientId:}") String clientId, - OAuth2AuthorizedClientService oAuth2AuthorizedClientService, - ClientRegistrationRepository clientRegistrationRepository) { - return new OAuth2AccessTokenInterceptor(clientId, oAuth2AuthorizedClientService, - clientRegistrationRepository); + @Value("${spring.cloud.openfeign.oauth2.clientRegistrationId:}") String clientRegistrationId, + OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + ClientRegistrationRepository clientRegistrationRepository) { + return new OAuth2AccessTokenInterceptor(clientRegistrationId, oAuth2AuthorizedClientService, + clientRegistrationRepository); } } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java index b421d0ae..dacd8b30 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java @@ -17,7 +17,6 @@ package org.springframework.cloud.openfeign.security; import java.net.URI; -import java.time.Instant; import java.util.Optional; import feign.RequestInterceptor; @@ -69,9 +68,7 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { private final String header; - private final String clientId; - - private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService; + private final String clientRegistrationId; private OAuth2AuthorizedClientManager authorizedClientManager; @@ -87,20 +84,20 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { this(null, oAuth2AuthorizedClientService, clientRegistrationRepository); } - public OAuth2AccessTokenInterceptor(String clientId, OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + public OAuth2AccessTokenInterceptor(String clientRegistrationId, + OAuth2AuthorizedClientService oAuth2AuthorizedClientService, ClientRegistrationRepository clientRegistrationRepository) { - this(BEARER, AUTHORIZATION, clientId, oAuth2AuthorizedClientService, clientRegistrationRepository); + this(BEARER, AUTHORIZATION, clientRegistrationId, oAuth2AuthorizedClientService, clientRegistrationRepository); } - public OAuth2AccessTokenInterceptor(String tokenType, String header, String clientId, + public OAuth2AccessTokenInterceptor(String tokenType, String header, String clientRegistrationId, OAuth2AuthorizedClientService oAuth2AuthorizedClientService, ClientRegistrationRepository clientRegistrationRepository) { this.tokenType = tokenType; this.header = header; - this.clientId = clientId; - this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService; + this.clientRegistrationId = clientRegistrationId; this.authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager( - clientRegistrationRepository, this.oAuth2AuthorizedClientService); + clientRegistrationRepository, oAuth2AuthorizedClientService); } @Override @@ -113,8 +110,8 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { public OAuth2AccessToken getToken(RequestTemplate template) { // If specified, try to use them to get token. - if (StringUtils.hasText(clientId)) { - OAuth2AccessToken token = getToken(clientId); + if (StringUtils.hasText(clientRegistrationId)) { + OAuth2AccessToken token = getToken(clientRegistrationId); if (token != null) { return token; } @@ -129,8 +126,8 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { throw new IllegalStateException("OAuth2 token has not been successfully acquired."); } - protected OAuth2AccessToken getToken(String clientId) { - if (!StringUtils.hasText(clientId)) { + protected OAuth2AccessToken getToken(String clientRegistrationId) { + if (!StringUtils.hasText(clientRegistrationId)) { return null; } @@ -139,26 +136,10 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { principal = ANONYMOUS_AUTHENTICATION; } - // Already exist - OAuth2AuthorizedClient oAuth2AuthorizedClient = oAuth2AuthorizedClientService.loadAuthorizedClient(clientId, - principal.getName()); - if (oAuth2AuthorizedClient != null) { - OAuth2AccessToken accessToken = oAuth2AuthorizedClient.getAccessToken(); - if (accessToken != null && notExpired(accessToken)) { - return accessToken; - } - } - - OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientId) + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId) .principal(principal).build(); OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest); - return Optional.ofNullable(authorizedClient).map(OAuth2AuthorizedClient::getAccessToken) - .filter(this::notExpired).orElse(null); - } - - protected boolean notExpired(OAuth2AccessToken token) { - return Optional.ofNullable(token).map(OAuth2AccessToken::getExpiresAt) - .map(expire -> expire.isAfter(Instant.now())).orElse(false); + return Optional.ofNullable(authorizedClient).map(OAuth2AuthorizedClient::getAccessToken).orElse(null); } private static String getServiceId(RequestTemplate template) { diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java index a8f1c919..b43fba31 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java @@ -139,13 +139,13 @@ class FeignAutoConfigurationTests { @Test void shouldInstantiateFeignOAuth2FeignRequestInterceptor() { runner.withPropertyValues("spring.cloud.openfeign.oauth2.enabled=true", - "spring.cloud.openfeign.oauth2.clientId=feign-client") - .withBean(OAuth2AuthorizedClientService.class, () -> mock(OAuth2AuthorizedClientService.class)) - .withBean(ClientRegistrationRepository.class, () -> mock(ClientRegistrationRepository.class)) - .run(ctx -> { - assertOauth2AccessTokenInterceptorExists(ctx); - assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue(ctx, "feign-client"); - }); + "spring.cloud.openfeign.oauth2.clientRegistrationId=feign-client") + .withBean(OAuth2AuthorizedClientService.class, () -> mock(OAuth2AuthorizedClientService.class)) + .withBean(ClientRegistrationRepository.class, () -> mock(ClientRegistrationRepository.class)) + .run(ctx -> { + assertOauth2AccessTokenInterceptorExists(ctx); + assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue(ctx, "feign-client"); + }); } private void assertOauth2FeignRequestInterceptorExists(ConfigurableApplicationContext ctx) { @@ -177,20 +177,19 @@ class FeignAutoConfigurationTests { } private void assertThatOauth2AccessTokenInterceptorHasSpecifiedIdsPropertyWithValue( - ConfigurableApplicationContext ctx, String expectedValue) { + ConfigurableApplicationContext ctx, String expectedValue) { final OAuth2AccessTokenInterceptor bean = ctx.getBean(OAuth2AccessTokenInterceptor.class); - assertThat(bean).hasFieldOrPropertyWithValue("clientId", expectedValue); + assertThat(bean).hasFieldOrPropertyWithValue("clientRegistrationId", expectedValue); } private void assertOnlyOneTargeterPresent(ConfigurableApplicationContext ctx, Class beanClass) { - assertThat(ctx.getBeansOfType(Targeter.class)).hasSize(1) - .hasValueSatisfying(new Condition<>( + assertThat(ctx.getBeansOfType(Targeter.class)).hasSize(1).hasValueSatisfying(new Condition<>( beanClass::isInstance, String.format("Targeter should be an instance of %s", beanClass))); } private void assertThatFeignCircuitBreakerTargeterHasGroupEnabledPropertyWithValue( - ConfigurableApplicationContext ctx, boolean expectedValue) { + ConfigurableApplicationContext ctx, boolean expectedValue) { final FeignCircuitBreakerTargeter bean = ctx.getBean(FeignCircuitBreakerTargeter.class); assertThat(bean).hasFieldOrPropertyWithValue("circuitBreakerGroupEnabled", expectedValue); } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java index 9a825e57..acedec4e 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java @@ -32,14 +32,11 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.util.AlternativeJdkIdGenerator; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -52,6 +49,9 @@ import static org.mockito.Mockito.when; */ class OAuth2AccessTokenInterceptorTests { + private final ClientRegistrationRepository mockClientRegistrationRepository = mock( + ClientRegistrationRepository.class); + private final OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock( OAuth2AuthorizedClientService.class); @@ -62,7 +62,7 @@ class OAuth2AccessTokenInterceptorTests { private RequestTemplate requestTemplate; - private static final String DEFAULT_CLIENT_ID = "feign-client"; + private static final String DEFAULT_CLIENT_REGISTRATION_ID = "feign-client"; @BeforeEach void setUp() { @@ -74,9 +74,8 @@ class OAuth2AccessTokenInterceptorTests { @Test void shouldThrowExceptionWhenNoTokenAcquired() { - when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).thenReturn(null); oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, - mock(ClientRegistrationRepository.class)); + mockClientRegistrationRepository); when(mockOAuth2AuthorizedClientManager.authorize(any())).thenReturn(null); oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); @@ -87,9 +86,20 @@ class OAuth2AccessTokenInterceptorTests { @Test void shouldAcquireValidToken() { - when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).thenReturn(null); oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, - mock(ClientRegistrationRepository.class)); + mockClientRegistrationRepository); + when(mockOAuth2AuthorizedClientManager.authorize(any())).thenReturn(validTokenOAuth2AuthorizedClient()); + oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); + + oAuth2AccessTokenInterceptor.apply(requestTemplate); + + assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); + } + + @Test + void shouldAcquireValidTokenFromServiceId() { + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, + mockClientRegistrationRepository); when(mockOAuth2AuthorizedClientManager.authorize( argThat((OAuth2AuthorizeRequest request) -> ("test").equals(request.getClientRegistrationId())))) .thenReturn(validTokenOAuth2AuthorizedClient()); @@ -100,37 +110,14 @@ class OAuth2AccessTokenInterceptorTests { assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); } - @Test - void shouldThrowExceptionWhenExpiredTokenAcquired() { - when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(anyString(), anyString())).thenReturn(null); - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, - mock(ClientRegistrationRepository.class)); - when(mockOAuth2AuthorizedClientManager.authorize(any())).thenReturn(expiredTokenOAuth2AuthorizedClient()); - oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); - - assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate)) - .withMessage("OAuth2 token has not been successfully acquired."); - } - - @Test - void shouldAcquireTokenFromAuthorizedClient() { - when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(eq("test"), anyString())) - .thenReturn(validTokenOAuth2AuthorizedClient()); - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, - mock(ClientRegistrationRepository.class)); - - oAuth2AccessTokenInterceptor.apply(requestTemplate); - - assertThat(requestTemplate.headers().get("Authorization")).contains("Bearer Valid Token"); - } - @Test void shouldAcquireValidTokenFromSpecifiedClientId() { - when(mockOAuth2AuthorizedClientService.loadAuthorizedClient(eq("testId"), anyString())) - .thenReturn(validTokenOAuth2AuthorizedClient()); - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor("testId", mockOAuth2AuthorizedClientService, - mock(ClientRegistrationRepository.class)); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(DEFAULT_CLIENT_REGISTRATION_ID, + mockOAuth2AuthorizedClientService, mockClientRegistrationRepository); + when(mockOAuth2AuthorizedClientManager + .authorize(argThat((OAuth2AuthorizeRequest request) -> (DEFAULT_CLIENT_REGISTRATION_ID) + .equals(request.getClientRegistrationId())))).thenReturn(validTokenOAuth2AuthorizedClient()); + oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); oAuth2AccessTokenInterceptor.apply(requestTemplate); @@ -142,23 +129,13 @@ class OAuth2AccessTokenInterceptorTests { Instant.now().plusSeconds(60L)); } - private OAuth2AccessToken expiredToken() { - return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "Expired Token", - Instant.now().minusSeconds(61L), Instant.now().minusSeconds(60L)); - } - private OAuth2AuthorizedClient validTokenOAuth2AuthorizedClient() { return new OAuth2AuthorizedClient(defaultClientRegistration(), "anonymousUser", validToken()); } - private OAuth2AuthorizedClient expiredTokenOAuth2AuthorizedClient() { - return new OAuth2AuthorizedClient(defaultClientRegistration(), "anonymousUser", expiredToken()); - } - private ClientRegistration defaultClientRegistration() { - return ClientRegistration.withRegistrationId(new AlternativeJdkIdGenerator().generateId().toString()) - .clientId(DEFAULT_CLIENT_ID).tokenUri("mock token uri") - .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).build(); + return ClientRegistration.withRegistrationId(DEFAULT_CLIENT_REGISTRATION_ID).clientId("clientId") + .tokenUri("mock token uri").authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).build(); } } From 77072d96e47b2e7cfcba85eb537601a1bad34b30 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Fri, 30 Sep 2022 14:30:30 +0200 Subject: [PATCH 7/7] Inject OAuth2AuthorizedClientManager via constructor. --- docs/src/main/asciidoc/_configprops.adoc | 5 ++- .../main/asciidoc/spring-cloud-openfeign.adoc | 16 +++++-- .../openfeign/FeignAutoConfiguration.java | 33 ++++++++++---- .../OAuth2AccessTokenInterceptor.java | 45 +++++++------------ .../OAuth2FeignRequestInterceptor.java | 1 + .../OAuth2FeignRequestInterceptorBuilder.java | 2 + ...uth2FeignRequestInterceptorConfigurer.java | 2 + ...itional-spring-configuration-metadata.json | 6 +-- .../FeignAutoConfigurationTests.java | 31 ++++++------- .../OAuth2AccessTokenInterceptorTests.java | 29 ++++-------- spring-cloud-openfeign-dependencies/pom.xml | 2 + 11 files changed, 89 insertions(+), 83 deletions(-) diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 80207091..31a6377d 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -4,7 +4,7 @@ |feign.autoconfiguration.jackson.enabled | `+++false+++` | If true, PageJacksonModule and SortJacksonModule bean will be provided for Jackson page decoding. |feign.circuitbreaker.alphanumeric-ids.enabled | `+++false+++` | If true, Circuit Breaker ids will only contain alphanumeric characters to allow for configuration via configuration properties. |feign.circuitbreaker.enabled | `+++false+++` | If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker. -|feign.circuitbreaker.group.enabled | `+++false+++` | If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker with with group. +|feign.circuitbreaker.group.enabled | `+++false+++` | If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker with group. |feign.client.config | | |feign.client.decode-slash | `+++true+++` | Feign clients do not encode slash `/` characters by default. To change this behavior, set the `decodeSlash` to `false`. |feign.client.default-config | `+++default+++` | @@ -31,8 +31,9 @@ |feign.httpclient.time-to-live | `+++900+++` | |feign.httpclient.time-to-live-unit | | |feign.metrics.enabled | `+++true+++` | Enables metrics capability for Feign. +|feign.oauth2.clientRegistrationId | | Provides a clientRegistrationId to be used with OAuth2. |feign.oauth2.enabled | `+++false+++` | Enables feign interceptor for managing oauth2 access token. |feign.oauth2.load-balanced | `+++false+++` | Enables load balancing for oauth2 access token provider. |feign.okhttp.enabled | `+++false+++` | Enables the use of the OK HTTP Client by Feign. -|=== \ No newline at end of file +|=== diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index 2c893da4..271f42c0 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -807,16 +807,26 @@ OAuth2 support can be enabled by setting following flag: ---- feign.oauth2.enabled=true ---- -When the flag is set to true, and the oauth2 client context resource details are present, a bean of class `OAuth2FeignRequestInterceptor` is created. Before each request, the interceptor resolves the required access token and includes it as a header. + +==== Deprecated OAuth2 Support + +When the flag is set to `true`, and `spring-security-oauth2-autoconfigure` is present in the classpath and the oauth2 client context resource details are present and `OAuth2ClientContext` and `OAuth2ProtectedResourceDetails` beans are present, a bean of class `OAuth2FeignRequestInterceptor` is created. Before each request, the interceptor resolves the required access token and includes it as a header. Sometimes, when load balancing is enabled for Feign clients, you may want to use load balancing for fetching access tokens, too. To do so, you should ensure that the load balancer is on the classpath (spring-cloud-starter-loadbalancer) and explicitly enable load balancing for OAuth2FeignRequestInterceptor by setting the following flag: ---- feign.oauth2.load-balanced=true +---- -When the flag is set to true, and the oauth2 client context resource details are present, a bean of class `OAuth2AccessTokenInterceptor` is created. Before each request, the interceptor resolves the required access token and includes it as a header. -`OAuth2AccessTokenInterceptor` uses the `AuthorizedClientServiceOAuth2AuthorizedClientManager` to get `OAuth2AuthorizedClient` that holds an `OAuth2AccessToken`. If the user has specified an OAuth2 `clientRegistrationId` using the `spring.cloud.openfeign.oauth2.clientRegistrationId` property, it will be used to retrieve the token. If the token is not retrieved or the `clientRegistrationId` has not been specified, the `serviceId` retrieved from the `url` host segment will be used. +WARNING:: The OAuth2 support as described above is now deprecated since `spring-security-oauth2-autoconfigure` has reached end of life. Please use the mode described below instead. + +==== Current OAuth2 Support + +When the `feign.client.refresh-enabled` flag is set to true, and `spring-security-oauth2-client` is present in the classpath, a bean of class `OAuth2AccessTokenInterceptor` is created. Before each request, the interceptor resolves the required access token and includes it as a header. +`OAuth2AccessTokenInterceptor` uses the `OAuth2AuthorizedClientManager` to get `OAuth2AuthorizedClient` that holds an `OAuth2AccessToken`. If the user has specified an OAuth2 `clientRegistrationId` using the `feign.oauth2.clientRegistrationId` property, it will be used to retrieve the token. If the token is not retrieved or the `clientRegistrationId` has not been specified, the `serviceId` retrieved from the `url` host segment will be used. TIP:: Using the `serviceId` as OAuth2 client registrationId is convenient for load-balanced Feign clients. For non-load-balanced ones, the property-based `clientRegistrationId` is a suitable approach. +TIP:: If you do not want to use the default setup for the `OAuth2AuthorizedClientManager`, you can just instantiate a bean of this type in your configuration. + === Transform the load-balanced HTTP request You can use the selected `ServiceInstance` to transform the load-balanced HTTP Request. diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java index 2bec6315..15333168 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java @@ -75,6 +75,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; import org.springframework.data.domain.Sort; +import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2ClientContext; @@ -352,7 +353,7 @@ public class FeignAutoConfiguration { @ConditionalOnClass(OAuth2ClientContext.class) @ConditionalOnProperty("feign.oauth2.enabled") @Deprecated // spring-security-oauth2 reached EOL - protected static class Oauth2FeignConfiguration { + protected static class DeprecatedOauth2FeignConfiguration { @ConditionalOnBean({ RetryLoadBalancerInterceptor.class, OAuth2ClientContext.class, OAuth2ProtectedResourceDetails.class }) @@ -374,20 +375,36 @@ public class FeignAutoConfiguration { @Bean @ConditionalOnMissingBean(OAuth2FeignRequestInterceptor.class) - @ConditionalOnBean({OAuth2ClientContext.class, OAuth2ProtectedResourceDetails.class}) + @ConditionalOnBean({ OAuth2ClientContext.class, OAuth2ProtectedResourceDetails.class }) public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext, - OAuth2ProtectedResourceDetails resource, List configurers) { + OAuth2ProtectedResourceDetails resource, List configurers) { return buildWithConfigurers(oAuth2ClientContext, resource, configurers); } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(OAuth2AuthorizedClientManager.class) + @ConditionalOnProperty("feign.oauth2.enabled") + protected static class Oauth2FeignConfiguration { + @Bean @ConditionalOnBean({ OAuth2AuthorizedClientService.class, ClientRegistrationRepository.class }) + @ConditionalOnMissingBean + OAuth2AuthorizedClientManager feignOAuth2AuthorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientService oAuth2AuthorizedClientService) { + return new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, + oAuth2AuthorizedClientService); + + } + + @Bean + @ConditionalOnBean(OAuth2AuthorizedClientManager.class) public OAuth2AccessTokenInterceptor defaultOAuth2AccessTokenInterceptor( - @Value("${spring.cloud.openfeign.oauth2.clientRegistrationId:}") String clientRegistrationId, - OAuth2AuthorizedClientService oAuth2AuthorizedClientService, - ClientRegistrationRepository clientRegistrationRepository) { - return new OAuth2AccessTokenInterceptor(clientRegistrationId, oAuth2AuthorizedClientService, - clientRegistrationRepository); + @Value("${feign.oauth2.clientRegistrationId:}") String clientRegistrationId, + OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) { + return new OAuth2AccessTokenInterceptor(clientRegistrationId, oAuth2AuthorizedClientManager); } } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java index dacd8b30..631edbc1 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java @@ -27,26 +27,23 @@ import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * A {@link RequestInterceptor} for OAuth2 Feign Requests. By default, it uses the - * {@link AuthorizedClientServiceOAuth2AuthorizedClientManager } to get - * {@link OAuth2AuthorizedClient } that holds an {@link OAuth2AccessToken }. If the user - * has specified an OAuth2 {@code clientId} using the - * {@code spring.cloud.openfeign.oauth2.clientId} property, it will be used to retrieve - * the token. If the token is not retrieved or the {@code clientId} has not been - * specified, the {@code serviceId} retrieved from the {@code url} host segment will be - * used. This approach is convenient for load-balanced Feign clients. For - * non-load-balanced ones, the property-based {@code clientId} is a suitable approach. + * {@link OAuth2AuthorizedClientManager } to get {@link OAuth2AuthorizedClient } that + * holds an {@link OAuth2AccessToken }. If the user has specified an OAuth2 + * {@code clientRegistrationId} using the {@code feign.oauth2.clientRegistrationId} + * property, it will be used to retrieve the token. If the token is not retrieved or the + * {@code clientRegistrationId} has not been specified, the {@code serviceId} retrieved + * from the {@code url} host segment will be used. This approach is convenient for + * load-balanced Feign clients. For non-load-balanced ones, the property-based + * {@code clientRegistrationId} is a suitable approach. * * @author Dangzhicairang(小水牛) * @author Olga Maciaszek-Sharma @@ -70,34 +67,26 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { private final String clientRegistrationId; - private OAuth2AuthorizedClientManager authorizedClientManager; - - public void setAuthorizedClientManager(OAuth2AuthorizedClientManager authorizedClientManager) { - this.authorizedClientManager = authorizedClientManager; - } + private final OAuth2AuthorizedClientManager authorizedClientManager; private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken("anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); - public OAuth2AccessTokenInterceptor(OAuth2AuthorizedClientService oAuth2AuthorizedClientService, - ClientRegistrationRepository clientRegistrationRepository) { - this(null, oAuth2AuthorizedClientService, clientRegistrationRepository); + public OAuth2AccessTokenInterceptor(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) { + this(null, oAuth2AuthorizedClientManager); } public OAuth2AccessTokenInterceptor(String clientRegistrationId, - OAuth2AuthorizedClientService oAuth2AuthorizedClientService, - ClientRegistrationRepository clientRegistrationRepository) { - this(BEARER, AUTHORIZATION, clientRegistrationId, oAuth2AuthorizedClientService, clientRegistrationRepository); + OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) { + this(BEARER, AUTHORIZATION, clientRegistrationId, oAuth2AuthorizedClientManager); } public OAuth2AccessTokenInterceptor(String tokenType, String header, String clientRegistrationId, - OAuth2AuthorizedClientService oAuth2AuthorizedClientService, - ClientRegistrationRepository clientRegistrationRepository) { + OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) { this.tokenType = tokenType; this.header = header; this.clientRegistrationId = clientRegistrationId; - this.authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager( - clientRegistrationRepository, oAuth2AuthorizedClientService); + this.authorizedClientManager = oAuth2AuthorizedClientManager; } @Override @@ -144,9 +133,9 @@ public class OAuth2AccessTokenInterceptor implements RequestInterceptor { private static String getServiceId(RequestTemplate template) { Target feignTarget = template.feignTarget(); - Assert.notNull(feignTarget, "feignTarget may not be null"); + Assert.notNull(feignTarget, "FeignTarget may not be null."); String url = feignTarget.url(); - Assert.hasLength(url, "url may not be empty"); + Assert.hasLength(url, "Url may not be empty."); final URI originalUri = URI.create(url); return originalUri.getHost(); } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptor.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptor.java index 2d26df89..78d0fa7e 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptor.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptor.java @@ -42,6 +42,7 @@ import org.springframework.security.oauth2.common.OAuth2AccessToken; * @author Joao Pedro Evangelista * @author Tim Ysewyn * @since 3.0.0 + * @deprecated in favour of {@link OAuth2AccessTokenInterceptor} */ @Deprecated // spring-security-oauth2 reached EOL public class OAuth2FeignRequestInterceptor implements RequestInterceptor { diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorBuilder.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorBuilder.java index 62beba0b..a80da25b 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorBuilder.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorBuilder.java @@ -39,7 +39,9 @@ import org.springframework.security.oauth2.client.token.grant.password.ResourceO * * @author Wojciech Mąka * @since 3.1.1 + * @deprecated since spring-security-oauth2 reached EOL */ +@Deprecated // spring-security-oauth2 reached EOL public class OAuth2FeignRequestInterceptorBuilder { private AccessTokenProvider accessTokenProvider; diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorConfigurer.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorConfigurer.java index 74ebbcdc..ed83952f 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorConfigurer.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2FeignRequestInterceptorConfigurer.java @@ -26,7 +26,9 @@ import org.springframework.security.oauth2.client.token.AccessTokenProvider; * * @author Wojciech Mąka * @since 3.1.1 + * @deprecated since spring-security-oauth2 reached EOL */ +@Deprecated // spring-security-oauth2 reached EOL @FunctionalInterface public interface OAuth2FeignRequestInterceptorConfigurer { diff --git a/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 88c42c56..960855b5 100644 --- a/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -23,7 +23,7 @@ { "name": "feign.circuitbreaker.group.enabled", "type": "java.lang.Boolean", - "description": "If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker with with group.", + "description": "If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker with group.", "defaultValue": "false" }, { @@ -81,9 +81,9 @@ "defaultValue": "false" }, { - "name": "feign.oauth2.registrationClientId", + "name": "feign.oauth2.clientRegistrationId", "type": "java.lang.String", - "description": "Provides a clientId to be used with OAuth2.", + "description": "Provides a clientRegistrationId to be used with OAuth2.", "defaultValue": "" } ] diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java index b43fba31..e91c4c50 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignAutoConfigurationTests.java @@ -127,19 +127,17 @@ class FeignAutoConfigurationTests { @Test void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithCustomAccessTokenProviderInterceptor() { - runner.withPropertyValues("feign.oauth2.enabled=true") - .withBean(MockOAuth2ClientContext.class, "token") - .withBean(BaseOAuth2ProtectedResourceDetails.class) - .withBean(CustomOAuth2FeignRequestInterceptorConfigurer.class).run(ctx -> { - assertOauth2FeignRequestInterceptorExists(ctx); - assertAccessTokenProviderInterceptorExists(ctx, BasicAuthenticationInterceptor.class); - }); + runner.withPropertyValues("feign.oauth2.enabled=true").withBean(MockOAuth2ClientContext.class, "token") + .withBean(BaseOAuth2ProtectedResourceDetails.class) + .withBean(CustomOAuth2FeignRequestInterceptorConfigurer.class).run(ctx -> { + assertOauth2FeignRequestInterceptorExists(ctx); + assertAccessTokenProviderInterceptorExists(ctx, BasicAuthenticationInterceptor.class); + }); } @Test void shouldInstantiateFeignOAuth2FeignRequestInterceptor() { - runner.withPropertyValues("spring.cloud.openfeign.oauth2.enabled=true", - "spring.cloud.openfeign.oauth2.clientRegistrationId=feign-client") + runner.withPropertyValues("feign.oauth2.enabled=true", "feign.oauth2.clientRegistrationId=feign-client") .withBean(OAuth2AuthorizedClientService.class, () -> mock(OAuth2AuthorizedClientService.class)) .withBean(ClientRegistrationRepository.class, () -> mock(ClientRegistrationRepository.class)) .run(ctx -> { @@ -154,21 +152,18 @@ class FeignAutoConfigurationTests { } private void assertAccessTokenProviderInterceptorExists(ConfigurableApplicationContext ctx, - Class clazz) { + Class clazz) { AssertableApplicationContext context = AssertableApplicationContext.get(() -> ctx); - assertThat(context).getBean(OAuth2FeignRequestInterceptor.class) - .extracting("accessTokenProvider") + assertThat(context).getBean(OAuth2FeignRequestInterceptor.class).extracting("accessTokenProvider") .extracting("interceptors").asList().first().isInstanceOf(clazz); } private void assertAccessTokenProviderInterceptorNotExists(ConfigurableApplicationContext ctx, - Class clazz) { + Class clazz) { AssertableApplicationContext context = AssertableApplicationContext.get(() -> ctx); - assertThat(context).getBean(OAuth2FeignRequestInterceptor.class) - .extracting("accessTokenProvider") - .extracting("interceptors").asList() - .filteredOn(obj -> clazz.isAssignableFrom(obj.getClass())) - .isEmpty(); + assertThat(context).getBean(OAuth2FeignRequestInterceptor.class).extracting("accessTokenProvider") + .extracting("interceptors").asList().filteredOn(obj -> clazz.isAssignableFrom(obj.getClass())) + .isEmpty(); } private void assertOauth2AccessTokenInterceptorExists(ConfigurableApplicationContext ctx) { diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java index acedec4e..686ebab5 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java @@ -27,9 +27,7 @@ import org.junit.jupiter.api.Test; import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; @@ -49,12 +47,6 @@ import static org.mockito.Mockito.when; */ class OAuth2AccessTokenInterceptorTests { - private final ClientRegistrationRepository mockClientRegistrationRepository = mock( - ClientRegistrationRepository.class); - - private final OAuth2AuthorizedClientService mockOAuth2AuthorizedClientService = mock( - OAuth2AuthorizedClientService.class); - private final OAuth2AuthorizedClientManager mockOAuth2AuthorizedClientManager = mock( OAuth2AuthorizedClientManager.class); @@ -74,10 +66,8 @@ class OAuth2AccessTokenInterceptorTests { @Test void shouldThrowExceptionWhenNoTokenAcquired() { - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, - mockClientRegistrationRepository); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientManager); when(mockOAuth2AuthorizedClientManager.authorize(any())).thenReturn(null); - oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); assertThatExceptionOfType(IllegalStateException.class) .isThrownBy(() -> oAuth2AccessTokenInterceptor.apply(requestTemplate)) @@ -86,10 +76,10 @@ class OAuth2AccessTokenInterceptorTests { @Test void shouldAcquireValidToken() { - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, - mockClientRegistrationRepository); - when(mockOAuth2AuthorizedClientManager.authorize(any())).thenReturn(validTokenOAuth2AuthorizedClient()); - oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientManager); + when(mockOAuth2AuthorizedClientManager.authorize( + argThat((OAuth2AuthorizeRequest request) -> ("test").equals(request.getClientRegistrationId())))) + .thenReturn(validTokenOAuth2AuthorizedClient()); oAuth2AccessTokenInterceptor.apply(requestTemplate); @@ -98,12 +88,10 @@ class OAuth2AccessTokenInterceptorTests { @Test void shouldAcquireValidTokenFromServiceId() { - oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientService, - mockClientRegistrationRepository); when(mockOAuth2AuthorizedClientManager.authorize( argThat((OAuth2AuthorizeRequest request) -> ("test").equals(request.getClientRegistrationId())))) .thenReturn(validTokenOAuth2AuthorizedClient()); - oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); + oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(mockOAuth2AuthorizedClientManager); oAuth2AccessTokenInterceptor.apply(requestTemplate); @@ -111,13 +99,12 @@ class OAuth2AccessTokenInterceptorTests { } @Test - void shouldAcquireValidTokenFromSpecifiedClientId() { + void shouldAcquireValidTokenFromSpecifiedClientRegistrationId() { oAuth2AccessTokenInterceptor = new OAuth2AccessTokenInterceptor(DEFAULT_CLIENT_REGISTRATION_ID, - mockOAuth2AuthorizedClientService, mockClientRegistrationRepository); + mockOAuth2AuthorizedClientManager); when(mockOAuth2AuthorizedClientManager .authorize(argThat((OAuth2AuthorizeRequest request) -> (DEFAULT_CLIENT_REGISTRATION_ID) .equals(request.getClientRegistrationId())))).thenReturn(validTokenOAuth2AuthorizedClient()); - oAuth2AccessTokenInterceptor.setAuthorizedClientManager(mockOAuth2AuthorizedClientManager); oAuth2AccessTokenInterceptor.apply(requestTemplate); diff --git a/spring-cloud-openfeign-dependencies/pom.xml b/spring-cloud-openfeign-dependencies/pom.xml index 70929ac7..f2064a01 100644 --- a/spring-cloud-openfeign-dependencies/pom.xml +++ b/spring-cloud-openfeign-dependencies/pom.xml @@ -19,6 +19,7 @@ 3.8.0 2.5.2 + 5.7.3 @@ -30,6 +31,7 @@ org.springframework.security spring-security-oauth2-client + ${spring-security-oauth2-client.version} org.springframework.cloud