From 66cc168a332d40b57c4499715217b040e08e043f Mon Sep 17 00:00:00 2001 From: dzcr <1137729123@qq.com> Date: Fri, 23 Sep 2022 17:28:16 +0800 Subject: [PATCH] step 1 --- spring-cloud-openfeign-core/pom.xml | 4 + .../openfeign/FeignAutoConfiguration.java | 47 ++--- .../OAuth2AccessTokenInterceptor.java | 175 ++++++++++++++++++ .../FeignAutoConfigurationTests.java | 91 +++------ ...viderWithLoadBalancerInterceptorTests.java | 12 +- ...erWithoutLoadBalancerInterceptorTests.java | 10 +- .../OAuth2AccessTokenInterceptorTests.java | 167 +++++++++++++++++ spring-cloud-openfeign-dependencies/pom.xml | 6 + 8 files changed, 400 insertions(+), 112 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 6ac76c20..9045b45c 100644 --- a/spring-cloud-openfeign-core/pom.xml +++ b/spring-cloud-openfeign-core/pom.xml @@ -215,6 +215,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 c40bfb66..889aa3ce 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 @@ -29,7 +29,6 @@ import com.fasterxml.jackson.databind.Module; import feign.Capability; import feign.Client; import feign.Feign; -import feign.RequestInterceptor; import feign.Target; import feign.hc5.ApacheHttp5Client; import feign.httpclient.ApacheHttpClient; @@ -50,19 +49,17 @@ 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; import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; -import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor; -import org.springframework.cloud.client.loadbalancer.RetryLoadBalancerInterceptor; import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionManagerFactory; 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.OAuth2FeignRequestInterceptor; -import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorConfigurer; +import org.springframework.cloud.openfeign.security.OAuth2AccessTokenInterceptor; import org.springframework.cloud.openfeign.support.FeignEncoderProperties; import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; import org.springframework.cloud.openfeign.support.PageJacksonModule; @@ -73,10 +70,8 @@ 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.OAuth2ClientContext; -import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; - -import static org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorBuilder.buildWithConfigurers; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; /** * @author Spencer Gibb @@ -90,6 +85,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) @@ -344,35 +340,20 @@ public class FeignAutoConfiguration { } @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(OAuth2ClientContext.class) + @EnableConfigurationProperties(OAuth2ClientProperties.class) @ConditionalOnProperty("spring.cloud.openfeign.oauth2.enabled") - @Deprecated // spring-security-oauth2 reached EOL protected static class Oauth2FeignConfiguration { - @ConditionalOnBean({ RetryLoadBalancerInterceptor.class, OAuth2ClientContext.class, - OAuth2ProtectedResourceDetails.class }) - @ConditionalOnProperty(value = "spring.cloud.openfeign.oauth2.load-balanced", havingValue = "true") @Bean - public OAuth2FeignRequestInterceptorConfigurer retryLoadBalancerInterceptorInjectingConfigurer( - final RetryLoadBalancerInterceptor loadBalancerInterceptor) { - return builder -> builder.withAccessTokenProviderInterceptors(loadBalancerInterceptor); - } + @ConditionalOnBean({ OAuth2AuthorizedClientService.class, ClientRegistrationRepository.class }) + public OAuth2AccessTokenInterceptor defaultOAuth2AccessTokenInterceptor( + @Value("${spring.cloud.openfeign.oauth2.specifiedClientIds:}") List specifiedClientIds, + OAuth2ClientProperties oAuth2ClientProperties, + OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + ClientRegistrationRepository clientRegistrationRepository) { - @ConditionalOnBean({ LoadBalancerInterceptor.class, OAuth2ClientContext.class, - OAuth2ProtectedResourceDetails.class }) - @ConditionalOnProperty(value = "spring.cloud.openfeign.oauth2.load-balanced", havingValue = "true") - @Bean - public OAuth2FeignRequestInterceptorConfigurer loadBalancerInterceptorInjectingConfigurer( - final LoadBalancerInterceptor loadBalancerInterceptor) { - return builder -> builder.withAccessTokenProviderInterceptors(loadBalancerInterceptor); - } - - @Bean - @ConditionalOnMissingBean(OAuth2FeignRequestInterceptor.class) - @ConditionalOnBean({ OAuth2ClientContext.class, OAuth2ProtectedResourceDetails.class }) - public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext, - OAuth2ProtectedResourceDetails resource, List configurers) { - return buildWithConfigurers(oAuth2ClientContext, resource, configurers); + 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..d695d871 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptor.java @@ -0,0 +1,175 @@ +/* + * 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 bcf67e35..222fa3f3 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; @@ -26,16 +28,11 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; -import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor; import org.springframework.cloud.openfeign.FeignAutoConfiguration.CircuitBreakerPresentFeignTargeterConfiguration.AlphanumericCircuitBreakerNameResolver; -import org.springframework.cloud.openfeign.security.MockOAuth2ClientContext; -import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor; -import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorBuilder; -import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptorConfigurer; +import org.springframework.cloud.openfeign.security.OAuth2AccessTokenInterceptor; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.http.client.ClientHttpRequestInterceptor; -import org.springframework.http.client.support.BasicAuthenticationInterceptor; -import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -46,6 +43,7 @@ import static org.mockito.Mockito.mock; * @author Andrii Bohutskyi * @author Kwangyong Kim * @author Wojciech Mąka + * @author Dangzhicairang(小水牛) */ class FeignAutoConfigurationTests { @@ -93,63 +91,30 @@ class FeignAutoConfigurationTests { @Test void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithoutInterceptors() { - runner.withPropertyValues("spring.cloud.openfeign.oauth2.enabled=true") - .withBean(MockOAuth2ClientContext.class, "token").withBean(BaseOAuth2ProtectedResourceDetails.class) - .withBean(LoadBalancerInterceptor.class, () -> mock(LoadBalancerInterceptor.class)).run(ctx -> { - assertOauth2FeignRequestInterceptorExists(ctx); - assertAccessTokenProviderInterceptorNotExists(ctx, LoadBalancerInterceptor.class); - }); - } - - @Test - void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithLoadBalancedInterceptor() { runner.withPropertyValues("spring.cloud.openfeign.oauth2.enabled=true", - "spring.cloud.openfeign.oauth2.load-balanced=true").withBean(MockOAuth2ClientContext.class, "token") - .withBean(BaseOAuth2ProtectedResourceDetails.class) - .withBean(LoadBalancerInterceptor.class, () -> mock(LoadBalancerInterceptor.class)).run(ctx -> { - assertOauth2FeignRequestInterceptorExists(ctx); - assertAccessTokenProviderInterceptorExists(ctx, LoadBalancerInterceptor.class); + "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"); + } + }); }); } - @Test - void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithoutLoadBalancedInterceptorIfNoBeanPresent() { - runner.withPropertyValues("spring.cloud.openfeign.oauth2.enabled=true", - "spring.cloud.openfeign.oauth2.load-balanced=true").withBean(MockOAuth2ClientContext.class, "token") - .withBean(BaseOAuth2ProtectedResourceDetails.class).run(ctx -> { - assertOauth2FeignRequestInterceptorExists(ctx); - assertAccessTokenProviderInterceptorNotExists(ctx, LoadBalancerInterceptor.class); - }); - } - - @Test - void shouldInstantiateFeignOAuth2FeignRequestInterceptorWithCustomAccessTokenProviderInterceptor() { - runner.withPropertyValues("spring.cloud.openfeign.oauth2.enabled=true") - .withBean(MockOAuth2ClientContext.class, "token").withBean(BaseOAuth2ProtectedResourceDetails.class) - .withBean(CustomOAuth2FeignRequestInterceptorConfigurer.class).run(ctx -> { - assertOauth2FeignRequestInterceptorExists(ctx); - assertAccessTokenProviderInterceptorExists(ctx, BasicAuthenticationInterceptor.class); - }); - } - - private void assertOauth2FeignRequestInterceptorExists(ConfigurableApplicationContext ctx) { + private void assertOauth2AccessTokenInterceptorExists(ConfigurableApplicationContext ctx) { AssertableApplicationContext context = AssertableApplicationContext.get(() -> ctx); - assertThat(context).hasSingleBean(OAuth2FeignRequestInterceptor.class); + assertThat(context).hasSingleBean(OAuth2AccessTokenInterceptor.class); } - private void assertAccessTokenProviderInterceptorExists(ConfigurableApplicationContext ctx, - Class clazz) { - AssertableApplicationContext context = AssertableApplicationContext.get(() -> ctx); - assertThat(context).getBean(OAuth2FeignRequestInterceptor.class).extracting("accessTokenProvider") - .extracting("interceptors").asList().first().isInstanceOf(clazz); - } - - private void assertAccessTokenProviderInterceptorNotExists(ConfigurableApplicationContext ctx, - Class clazz) { - AssertableApplicationContext context = AssertableApplicationContext.get(() -> ctx); - assertThat(context).getBean(OAuth2FeignRequestInterceptor.class).extracting("accessTokenProvider") - .extracting("interceptors").asList().filteredOn(obj -> clazz.isAssignableFrom(obj.getClass())) - .isEmpty(); + 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) { @@ -179,14 +144,4 @@ class FeignAutoConfigurationTests { } - static class CustomOAuth2FeignRequestInterceptorConfigurer implements OAuth2FeignRequestInterceptorConfigurer { - - @Override - public void customize(OAuth2FeignRequestInterceptorBuilder requestInterceptorBuilder) { - requestInterceptorBuilder - .withAccessTokenProviderInterceptors(new BasicAuthenticationInterceptor("username", "password")); - } - - } - } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithLoadBalancerInterceptorTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithLoadBalancerInterceptorTests.java index cec77e98..767ab515 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithLoadBalancerInterceptorTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithLoadBalancerInterceptorTests.java @@ -41,11 +41,11 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen * @author Wojciech Mąka */ @SpringBootTest(classes = AccessTokenProviderWithLoadBalancerInterceptorTests.Application.class, - webEnvironment = RANDOM_PORT, - value = { "security.oauth2.client.id=test-service", "security.oauth2.client.client-id=test-service", - "security.oauth2.client.client-secret=test-service", - "security.oauth2.client.grant-type=client_credentials", "spring.cloud.openfeign.oauth2.enabled=true", - "spring.cloud.openfeign.oauth2.load-balanced=true" }) + webEnvironment = RANDOM_PORT, + value = { "security.oauth2.client.id=test-service", "security.oauth2.client.client-id=test-service", + "security.oauth2.client.client-secret=test-service", + "security.oauth2.client.grant-type=client_credentials", "spring.cloud.openfeign.oauth2.enabled=true", + "spring.cloud.openfeign.oauth2.load-balanced=true" }) @DirtiesContext public class AccessTokenProviderWithLoadBalancerInterceptorTests { @@ -70,7 +70,7 @@ public class AccessTokenProviderWithLoadBalancerInterceptorTests { @EnableAutoConfiguration @RestController @EnableFeignClients( - clients = { AccessTokenProviderWithLoadBalancerInterceptorTests.Application.SampleClient.class }) + clients = { AccessTokenProviderWithLoadBalancerInterceptorTests.Application.SampleClient.class }) protected static class Application { @GetMapping("/foo") diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithoutLoadBalancerInterceptorTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithoutLoadBalancerInterceptorTests.java index 4693a137..d6c55f82 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithoutLoadBalancerInterceptorTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/AccessTokenProviderWithoutLoadBalancerInterceptorTests.java @@ -41,10 +41,10 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen * @author Wojciech Mąka */ @SpringBootTest(classes = AccessTokenProviderWithoutLoadBalancerInterceptorTests.Application.class, - webEnvironment = RANDOM_PORT, - value = { "security.oauth2.client.id=test-service", "security.oauth2.client.client-id=test-service", - "security.oauth2.client.client-secret=test-service", - "security.oauth2.client.grant-type=client_credentials", "spring.cloud.openfeign.oauth2.enabled=true" }) + webEnvironment = RANDOM_PORT, + value = { "security.oauth2.client.id=test-service", "security.oauth2.client.client-id=test-service", + "security.oauth2.client.client-secret=test-service", + "security.oauth2.client.grant-type=client_credentials", "spring.cloud.openfeign.oauth2.enabled=true" }) @DirtiesContext public class AccessTokenProviderWithoutLoadBalancerInterceptorTests { @@ -70,7 +70,7 @@ public class AccessTokenProviderWithoutLoadBalancerInterceptorTests { @EnableAutoConfiguration @RestController @EnableFeignClients( - clients = { AccessTokenProviderWithoutLoadBalancerInterceptorTests.Application.SampleClient.class }) + clients = { AccessTokenProviderWithoutLoadBalancerInterceptorTests.Application.SampleClient.class }) protected static class Application { @GetMapping("/foo") 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..3066bd61 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/security/OAuth2AccessTokenInterceptorTests.java @@ -0,0 +1,167 @@ +/* + * 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 9c10c686..98cd1aa5 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