Moves classes from spring-cloud-security to spring-cloud-commons

This commit is contained in:
Marcin Grzejszczak
2020-12-18 17:21:10 +01:00
parent 18b7e0cc9d
commit 1f0f750707
14 changed files with 754 additions and 3 deletions

View File

@@ -1146,6 +1146,95 @@ myrandom=${cachedrandom.appname.value}
----
====
[[spring-cloud-security]]
== Security
[[spring-cloud-security-single-sign-on]]
=== Single Sign On
NOTE: All of the OAuth2 SSO and resource server features moved to Spring Boot
in version 1.3. You can find documentation in the
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/[Spring Boot user guide].
[[spring-cloud-security-client-token-relay]]
==== Client Token Relay
If your app is a user facing OAuth2 client (i.e. has declared
`@EnableOAuth2Sso` or `@EnableOAuth2Client`) then it has an
`OAuth2ClientContext` in request scope from Spring Boot. You can
create your own `OAuth2RestTemplate` from this context and an
autowired `OAuth2ProtectedResourceDetails`, and then the context will
always forward the access token downstream, also refreshing the access
token automatically if it expires. (These are features of Spring
Security and Spring Boot.)
[[spring-cloud-security-resource-server-token-relay]]
==== Resource Server Token Relay
If your app has `@EnableResourceServer` you might want to relay the
incoming token downstream to other services. If you use a
`RestTemplate` to contact the downstream services then this is just a
matter of how to create the template with the right context.
If your service uses `UserInfoTokenServices` to authenticate incoming
tokens (i.e. it is using the `security.oauth2.user-info-uri`
configuration), then you can simply create an `OAuth2RestTemplate`
using an autowired `OAuth2ClientContext` (it will be populated by the
authentication process before it hits the backend code). Equivalently
(with Spring Boot 1.4), you could inject a
`UserInfoRestTemplateFactory` and grab its `OAuth2RestTemplate` in
your configuration. For example:
.MyConfiguration.java
[source,java]
----
@Bean
public OAuth2RestTemplate restTemplate(UserInfoRestTemplateFactory factory) {
return factory.getUserInfoRestTemplate();
}
----
This rest template will then have the same `OAuth2ClientContext`
(request-scoped) that is used by the authentication filter, so you can
use it to send requests with the same access token.
If your app is not using `UserInfoTokenServices` but is still a client
(i.e. it declares `@EnableOAuth2Client` or `@EnableOAuth2Sso`), then
with Spring Security Cloud any `OAuth2RestOperations` that the user
creates from an `@Autowired` `OAuth2Context` will also forward
tokens. This feature is implemented by default as an MVC handler
interceptor, so it only works in Spring MVC. If you are not using MVC
you could use a custom filter or AOP interceptor wrapping an
`AccessTokenContextRelay` to provide the same feature.
Here's a basic
example showing the use of an autowired rest template created
elsewhere ("foo.com" is a Resource Server accepting the same tokens as
the surrounding app):
.MyController.java
[source,java]
----
@Autowired
private OAuth2RestOperations restTemplate;
@RequestMapping("/relay")
public String relay() {
ResponseEntity<String> response =
restTemplate.getForEntity("https://foo.com/bar", String.class);
return "Success! (" + response.getBody() + ")";
}
----
If you don't want to forward tokens (and that is a valid
choice, since you might want to act as yourself, rather than the
client that sent you the token), then you only need to create your own
`OAuth2Context` instead of autowiring the default one.
Feign clients will also pick up an interceptor that uses the
`OAuth2ClientContext` if it is available, so they should also do a
token relay anywhere where a `RestTemplate` would.
== Configuration Properties
To see the list of all Spring Cloud Commons related configuration properties please check link:appendix.html[the Appendix page].

View File

@@ -28,6 +28,7 @@
<properties>
<bintray.package>commons</bintray.package>
<evictor.version>1.0.0</evictor.version>
<spring-security-oauth2-autoconfigure.version>2.1.2.RELEASE</spring-security-oauth2-autoconfigure.version>
</properties>
<build>
<plugins>
@@ -150,6 +151,11 @@
<artifactId>spring-cloud-test-support</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${spring-security-oauth2-autoconfigure.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>

View File

@@ -108,6 +108,11 @@
<artifactId>spring-boot-starter-hateoas</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<!-- REMOVE after boot build updates -->
<dependency>
<groupId>org.springframework.hateoas</groupId>

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2012-2015 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.commons.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
/**
* Convenience class for relaying an access token from the {@link SecurityContext} to the
* {@link OAuth2ClientContext}. If successful then subsequent calls to an
* {@link OAuth2RestTemplate} using the context contained here will use the same access
* token. This is mostly useful for relaying calls to a resource server downstream to
* other resource servers. If the access token expires there is no way to refresh it, so
* expect an exception from downstream (propagating it to the caller is the best strategy,
* so they can refresh it and try again).
*
* @author Dave Syer
*
*/
public class AccessTokenContextRelay {
private OAuth2ClientContext context;
public AccessTokenContextRelay(OAuth2ClientContext context) {
this.context = context;
}
/**
* Attempt to copy an access token from the security context into the oauth2 context.
* @return true if the token was copied
*/
public boolean copyToken() {
if (context.getAccessToken() == null) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
Object details = authentication.getDetails();
if (details instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails holder = (OAuth2AuthenticationDetails) details;
String token = holder.getTokenValue();
DefaultOAuth2AccessToken accessToken = new DefaultOAuth2AccessToken(token);
String tokenType = holder.getTokenType();
if (tokenType != null) {
accessToken.setTokenType(tokenType);
}
context.setAccessToken(accessToken);
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2012-2015 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.commons.security;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfiguration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
/**
* Adds an MVC interceptor for relaying OAuth2 access tokens into the client context (if
* there is one). In this way an incoming request to a resource server can be relayed
* downstream just be using <code>@EnableOAuth2Client</code> and an
* <code>OAuth2RestTemplate</code>. An MVC interceptor is used so as to have a minimal
* impact on the call stack. If you are not using MVC you could use a custom filter or AOP
* interceptor wrapping the same call to an {@link AccessTokenContextRelay}.
*
* <br/>
*
* N.B. an app that is using {@link UserInfoTokenServices} generally doesn't need this
* interceptor, but it doesn't hurt to include it.
*
* @author Dave Syer
*
*/
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(OAuth2AutoConfiguration.class)
@ResourceServerTokenRelayAutoConfiguration.ConditionalOnOAuth2ClientInResourceServer
@ConditionalOnClass(ResourceServerConfiguration.class)
@ConditionalOnWebApplication
@ConditionalOnProperty(value = "spring.cloud.mvc.token-relay.enabled=true", matchIfMissing = true)
public class ResourceServerTokenRelayAutoConfiguration {
@Bean
public AccessTokenContextRelay accessTokenContextRelay(OAuth2ClientContext context) {
return new AccessTokenContextRelay(context);
}
/**
* A {@link WebMvcConfigurer} for the access token interceptor.
*
* @author Dave Syer
*
*/
@Configuration(proxyBeanMethods = false)
public static class ResourceServerTokenRelayRegistrationAutoConfiguration implements WebMvcConfigurer {
@Autowired
AccessTokenContextRelay accessTokenContextRelay;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(
new HandlerInterceptorAdapter() {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
accessTokenContextRelay.copyToken();
return true;
}
}
);
}
}
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OAuth2OnClientInResourceServerCondition.class)
@interface ConditionalOnOAuth2ClientInResourceServer {
}
private static class OAuth2OnClientInResourceServerCondition extends AllNestedConditions {
OAuth2OnClientInResourceServerCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnBean(ResourceServerConfiguration.class)
static class Server {
}
@ConditionalOnBean(OAuth2ClientConfiguration.class)
static class Client {
}
}
}

View File

@@ -15,7 +15,8 @@ org.springframework.cloud.client.serviceregistry.ServiceRegistryAutoConfiguratio
org.springframework.cloud.commons.httpclient.HttpClientConfiguration,\
org.springframework.cloud.commons.util.UtilAutoConfiguration,\
org.springframework.cloud.configuration.CompatibilityVerifierAutoConfiguration,\
org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration
org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration,\
org.springframework.cloud.commons.security.ResourceServerTokenRelayAutoConfiguration
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.cloud.client.HostInfoEnvironmentPostProcessor

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2015-2015 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.commons.security;
import org.junit.After;
import org.junit.Test;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
/**
* @author Dave Syer
*
*/
public class ResourceServerTokenRelayAutoConfigurationTests {
private ConfigurableApplicationContext context;
@After
public void close() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void clientNotConfigured() {
this.context = new SpringApplicationBuilder(NoClientConfiguration.class)
.properties("spring.config.name=test", "server.port=0", "spring.cloud.mvc.token-relay.enabled=true",
"security.oauth2.resource.userInfoUri:https://example.com")
.run();
assertThat(this.context.containsBean("loadBalancedOauth2RestTemplate")).isFalse();
}
@Test
public void clientConfigured() throws Exception {
this.context = new SpringApplicationBuilder(ClientConfiguration.class).properties("spring.config.name=test",
"server.port=0", "security.oauth2.resource.userInfoUri:https://example.com",
"security.oauth2.client.clientId=foo").run();
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest()));
OAuth2ClientContext client = this.context.getBean(OAuth2ClientContext.class);
assertThat(client.getAccessToken()).isNull();
UserInfoTokenServices services = context.getBean(UserInfoTokenServices.class);
OAuth2RestTemplate template = (OAuth2RestTemplate) ReflectionTestUtils.getField(services, "restTemplate");
MockRestServiceServer server = MockRestServiceServer.createServer(template);
server.expect(requestTo("https://example.com"))
.andRespond(withSuccess("{\"id\":\"user\"}", MediaType.APPLICATION_JSON));
services.loadAuthentication("FOO");
assertThat(client.getAccessToken().getValue()).isEqualTo("FOO");
server.verify();
}
@EnableAutoConfiguration
@Configuration(proxyBeanMethods = false)
@EnableResourceServer
protected static class NoClientConfiguration {
}
@EnableAutoConfiguration
@Configuration(proxyBeanMethods = false)
@EnableResourceServer
@EnableOAuth2Sso
protected static class ClientConfiguration {
@Bean
public OAuth2RestTemplate oauth2RestTemplate(OAuth2ProtectedResourceDetails resource,
OAuth2ClientContext oauth2Context) {
return new OAuth2RestTemplate(resource, oauth2Context);
}
}
}

View File

@@ -0,0 +1,145 @@
/*
* Copyright 2015-2015 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.commons.security.tokenrelay;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.context.TestComponent;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.cloud.commons.security.AccessTokenContextRelay;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
/**
* @author Peter Szanto (spring@szantocsalad.hu)
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = { "security.oauth2.resource.jwt.keyValue=secret", "spring.cloud.mvc.token-relay.enabled=true",
"spring.autoconfigure.exclude=" })
public class ResourceServerTokenRelayTests {
protected static final String TOKEN_VALID_UNTIL_2085 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
+ "eyJleHAiOjM2NDA2ODU4ODIsInVzZXJfbmFtZSI6InJlYWRlciIsImF1dGhvcml0aWVzIjpbIlJPTEVfUkVBREVSIl0s"
+ "Imp0aSI6ImRkOTAzZGM2LTI0NDctNDViMi04MDZjLTIzZjU3ODVhNGQ4MCIsImNsaWVudF9pZCI6IndlYi1hcHAiLCJzY29wZSI6WyJyZWFkIl19."
+ "6hoNtxmN1_o5Ki0D0ae4amSOTRmit3pmaqv-z1-Qk4Y";
protected static final String AUTH_HEADER_TO_BE_RELAYED = "Bearer " + TOKEN_VALID_UNTIL_2085;
protected static final String TEST_RESPONSE = "[\"test response\"]";
@Autowired
private TestRestTemplate testRestTemplate;
@Autowired
private MockRestServiceServer mockServerToReceiveRelay;
@SpyBean
AccessTokenContextRelay accessTokenContextRelay;
@Test
public void tokenRelayJWT() throws Exception {
mockServerToReceiveRelay.expect(requestTo("https://example.com/test"))
.andExpect(header("authorization", AUTH_HEADER_TO_BE_RELAYED))
.andRespond(withSuccess(TEST_RESPONSE, MediaType.APPLICATION_JSON));
HttpEntity<String> authorizationHeader = createAuthorizationHeader();
ResponseEntity<String> exchange = testRestTemplate.exchange("/token-relay", HttpMethod.GET, authorizationHeader,
String.class);
assertThat(exchange.getStatusCodeValue()).isEqualTo(HttpStatus.OK.value());
assertThat(exchange.getBody()).isEqualTo(TEST_RESPONSE);
mockServerToReceiveRelay.verify();
verify(accessTokenContextRelay).copyToken();
}
private HttpEntity<String> createAuthorizationHeader() {
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", AUTH_HEADER_TO_BE_RELAYED);
return new HttpEntity<String>("parameters", headers);
}
@SpringBootApplication
@TestConfiguration
@EnableResourceServer
@ComponentScan(basePackageClasses = TokenRelayTestController.class)
@EnableOAuth2Client
protected static class ClientConfiguration {
@Bean
public OAuth2RestTemplate oauth2RestTemplate(OAuth2ProtectedResourceDetails resource,
OAuth2ClientContext oauth2Context) {
return new OAuth2RestTemplate(resource, oauth2Context);
}
@Bean
public MockRestServiceServer mockRestServiceServer(OAuth2RestTemplate template) {
return MockRestServiceServer.createServer(template);
}
}
@RestController
@TestComponent
protected static class TokenRelayTestController {
@Autowired
OAuth2RestTemplate oAuth2RestTemplate;
@GetMapping("/token-relay")
public String callAnotherService() {
return oAuth2RestTemplate.getForEntity("https://example.com/test", String.class).getBody();
}
}
}

View File

@@ -6,4 +6,7 @@ debug:true
spring.cloud.hypermedia.refresh.initial-delay=50000
spring.cloud.hypermedia.refresh.fixed-delay=10000
management.security.enabled=false
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration,org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration
security.oauth2.resource.tokenRelay=false
security.oauth2.client.clientId: acme
security.oauth2.client.clientSecret: acmesecret

View File

@@ -82,11 +82,21 @@
<artifactId>spring-retry</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-test-support</artifactId>

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2015-2015 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.loadbalancer.security;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoRestTemplateCustomizer;
import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor;
import org.springframework.cloud.client.loadbalancer.RetryLoadBalancerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
/**
* @author Dave Syer
*
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OAuth2RestTemplate.class)
@ConditionalOnProperty("spring.cloud.oauth2.load-balanced.enabled=true")
@AutoConfigureAfter(OAuth2AutoConfiguration.class)
public class OAuth2LoadBalancerClientAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(LoadBalancerInterceptor.class)
protected static class UserInfoLoadBalancerConfig {
@Bean
public UserInfoRestTemplateCustomizer loadBalancedUserInfoRestTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return new UserInfoRestTemplateCustomizer() {
@Override
public void customize(OAuth2RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>(restTemplate.getInterceptors());
interceptors.add(loadBalancerInterceptor);
restTemplate.setInterceptors(interceptors);
}
};
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(RetryLoadBalancerInterceptor.class)
protected static class UserInfoRetryLoadBalancerConfig {
@Bean
public UserInfoRestTemplateCustomizer retryLoadBalancedUserInfoRestTemplateCustomizer(
final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
return new UserInfoRestTemplateCustomizer() {
@Override
public void customize(OAuth2RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>(restTemplate.getInterceptors());
interceptors.add(loadBalancerInterceptor);
restTemplate.setInterceptors(interceptors);
}
};
}
}
}

View File

@@ -2,4 +2,5 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration,\
org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientAutoConfiguration,\
org.springframework.cloud.loadbalancer.config.LoadBalancerCacheAutoConfiguration
org.springframework.cloud.loadbalancer.config.LoadBalancerCacheAutoConfiguration,\
org.springframework.cloud.loadbalancer.security.OAuth2LoadBalancerClientAutoConfiguration

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2015-2015 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.loadbalancer.security;
import java.net.URI;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoRestTemplateFactory;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.test.ClassPathExclusions;
import org.springframework.cloud.test.ModifiedClassPathRunner;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Dave Syer
*
*/
@RunWith(ModifiedClassPathRunner.class)
@ClassPathExclusions("spring-retry-*.jar")
public class OAuth2LoadBalancerClientAutoConfigurationTests {
private ConfigurableApplicationContext context;
@Rule
public ExpectedException expected = ExpectedException.none();
@After
public void close() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void userInfoNotLoadBalanced() {
this.context = new SpringApplicationBuilder(ClientConfiguration.class).properties("spring.config.name=test",
"server.port=0", "security.oauth2.resource.userInfoUri:https://example.com").run();
assertThat(this.context.containsBean("loadBalancedUserInfoRestTemplateCustomizer")).isFalse();
assertThat(this.context.containsBean("retryLoadBalancedUserInfoRestTemplateCustomizer")).isFalse();
}
@Test
public void userInfoLoadBalancedNoRetry() throws Exception {
this.context = new SpringApplicationBuilder(ClientConfiguration.class).properties("spring.config.name=test",
"server.port=0", "security.oauth2.resource.userInfoUri:https://nosuchservice",
"spring.cloud.oauth2.load-balanced.enabled=true").run();
assertThat(this.context.containsBean("loadBalancedUserInfoRestTemplateCustomizer")).isTrue();
assertThat(this.context.containsBean("retryLoadBalancedUserInfoRestTemplateCustomizer")).isFalse();
OAuth2RestTemplate template = this.context.getBean(UserInfoRestTemplateFactory.class).getUserInfoRestTemplate();
ClientHttpRequest request = template.getRequestFactory().createRequest(new URI("https://nosuchservice"),
HttpMethod.GET);
expected.expectMessage("No instances available for nosuchservice");
request.execute();
}
@EnableAutoConfiguration
@Configuration(proxyBeanMethods = false)
@EnableOAuth2Sso
protected static class ClientConfiguration {
}
}

View File

@@ -25,3 +25,7 @@ spring:
uri: http://hhost
- service-id: myservice
uri: http://ihost
autoconfigure.exclude:
- org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
- org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration
- org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration