Migrates token relay from spring-cloud-gateway-server-security.

It moves it to spring-cloud-gateway-server

See gh-1975
This commit is contained in:
spencergibb
2020-10-09 17:39:55 -04:00
parent dfa048faad
commit 05e3bafde7
11 changed files with 122 additions and 217 deletions

View File

@@ -1644,14 +1644,12 @@ it is proxying. To add this functionlity to gateway you need to add the
.App.java
[source,java]
----
@Autowired
private TokenRelayGatewayFilterFactory filterFactory;
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("resource", r -> r.path("/resource")
.filters(f -> f.filter(filterFactory.apply()))
.filters(f -> f.tokenRelay())
.uri("http://localhost:9000"))
.build();
}
@@ -1680,7 +1678,7 @@ pass the authentication token downstream to the services (in this case
To enable this for Spring Cloud Gateway add the following dependencies
- `org.springframework.cloud:spring-cloud-gateway-server-security`
- `org.springframework.boot:spring-boot-starter-oauth2-client`
How does it work? The
{githubmaster}/src/main/java/org/springframework/cloud/gateway/security/TokenRelayGatewayFilterFactory.java[filter]
@@ -1689,6 +1687,8 @@ and puts it in a request header for the downstream requests.
For a full working sample see https://github.com/spring-cloud-samples/sample-gateway-oauth2login[this project].
NOTE: A `TokenRelayGatewayFilterFactory` bean will only be created if the proper `spring.security.oauth2.client.*` properties are set which will trigger creation of a `ReactiveClientRegistrationRepository` bean.
NOTE: The default implementation of `ReactiveOAuth2AuthorizedClientService` used by `TokenRelayGatewayFilterFactory`
uses an in-memory data store. You will need to provide your own implementation `ReactiveOAuth2AuthorizedClientService`
if you need a more robust solution.

View File

@@ -1,81 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>3.0.0-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-gateway-server-security</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Gateway Server Security</name>
<description>Spring Cloud Gateway Server Security</description>
<properties>
<main.basedir>${basedir}/..</main.basedir>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</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-webflux</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-test-support</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,65 +0,0 @@
/*
* Copyright 2013-2014 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.gateway.security;
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.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.web.server.SecurityWebFilterChain;
/**
* @author Dave Syer
*
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@ConditionalOnClass({ GatewayFilter.class, OAuth2AuthorizedClient.class, SecurityWebFilterChain.class,
SecurityProperties.class })
@ConditionalOnWebApplication(type = Type.REACTIVE)
public class TokenRelayAutoConfiguration {
@Bean
public TokenRelayGatewayFilterFactory tokenRelayGatewayFilterFactory(
ReactiveOAuth2AuthorizedClientManager clientManager) {
return new TokenRelayGatewayFilterFactory(clientManager);
}
@Bean
public ReactiveOAuth2AuthorizedClientManager gatewayReactiveOAuth2AuthorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder
.builder().authorizationCode().refreshToken().build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}

View File

@@ -1,3 +0,0 @@
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.gateway.security.TokenRelayAutoConfiguration

View File

@@ -1,59 +0,0 @@
/*
* Copyright 2014-2018 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.gateway.security;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Spencer Gibb
*
*/
public class TokenRelayAutoConfigurationTests {
@Test
public void beansAreCreated() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class,
ReactiveOAuth2ClientAutoConfiguration.class, TokenRelayAutoConfiguration.class))
.withPropertyValues(
"spring.security.oauth2.client.provider[testprovider].authorization-uri=http://localhost",
"spring.security.oauth2.client.provider[testprovider].token-uri=http://localhost/token",
"spring.security.oauth2.client.registration[test].provider=testprovider",
"spring.security.oauth2.client.registration[test].authorization-grant-type=authorization_code",
"spring.security.oauth2.client.registration[test].redirect-uri=http://localhost/redirect",
"spring.security.oauth2.client.registration[test].client-id=login-client")
.withUserConfiguration(TestConfig.class).withPropertyValues("debug=true").run(context -> {
assertThat(context).hasSingleBean(ReactiveOAuth2AuthorizedClientManager.class);
assertThat(context).hasSingleBean(TokenRelayGatewayFilterFactory.class);
});
}
@Configuration
protected static class TestConfig {
}
}

View File

@@ -27,6 +27,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>

View File

@@ -45,6 +45,7 @@ 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.condition.NoneNestedConditions;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.embedded.NettyWebServerFactoryCustomizer;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
@@ -96,6 +97,7 @@ import org.springframework.cloud.gateway.filter.factory.SetRequestHostHeaderGate
import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.TokenRelayGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.GzipMessageBodyResolver;
import org.springframework.cloud.gateway.filter.factory.rewrite.MessageBodyDecoder;
import org.springframework.cloud.gateway.filter.factory.rewrite.MessageBodyEncoder;
@@ -148,6 +150,14 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.env.Environment;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.Validator;
@@ -831,4 +841,31 @@ public class GatewayAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@ConditionalOnClass({ OAuth2AuthorizedClient.class, SecurityWebFilterChain.class, SecurityProperties.class })
@ConditionalOnEnabledFilter(TokenRelayGatewayFilterFactory.class)
@ConditionalOnBean(ReactiveClientRegistrationRepository.class)
protected static class TokenRelayConfiguration {
@Bean
public TokenRelayGatewayFilterFactory tokenRelayGatewayFilterFactory(
ReactiveOAuth2AuthorizedClientManager clientManager) {
return new TokenRelayGatewayFilterFactory(clientManager);
}
@Bean
public ReactiveOAuth2AuthorizedClientManager gatewayReactiveOAuth2AuthorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder
.builder().authorizationCode().refreshToken().build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
}

View File

@@ -14,24 +14,21 @@
* limitations under the License.
*/
package org.springframework.cloud.gateway.security;
package org.springframework.cloud.gateway.filter.factory;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Joe Grandja
*/
@Component
public class TokenRelayGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
private final ReactiveOAuth2AuthorizedClientManager clientManager;

View File

@@ -67,6 +67,7 @@ import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGateway
import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SpringCloudCircuitBreakerFilterFactory;
import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.TokenRelayGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction;
@@ -700,6 +701,21 @@ public class GatewayFilterSpec extends UriSpec {
return filter(getBean(RequestHeaderSizeGatewayFilterFactory.class).apply(c -> c.setMaxSize(size)));
}
/**
* A filter that enables token relay.
* @return a {@link GatewayFilterSpec} that can be used to apply additional filters
*/
public GatewayFilterSpec tokenRelay() {
try {
return filter(getBean(TokenRelayGatewayFilterFactory.class).apply(o -> {
}));
}
catch (NoSuchBeanDefinitionException e) {
throw new IllegalStateException("No TokenRelayGatewayFilterFactory bean was found. Did you include the "
+ "org.springframework.boot:spring-boot-starter-oauth2-client dependency?");
}
}
/**
* Adds hystrix execution exception headers to fallback request. Depends on @{code
* org.springframework.cloud::spring-cloud-starter-netflix-hystrix} being on the

View File

@@ -27,18 +27,26 @@ import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfigu
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint;
import org.springframework.cloud.gateway.actuate.GatewayLegacyControllerEndpoint;
import org.springframework.cloud.gateway.filter.factory.TokenRelayGatewayFilterFactory;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.GatewayFilterSpec;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.web.filter.reactive.HiddenHttpMethodFilter;
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
import org.springframework.web.reactive.socket.server.upgrade.ReactorNettyRequestUpgradeStrategy;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class GatewayAutoConfigurationTests {
@@ -142,12 +150,62 @@ public class GatewayAutoConfigurationTests {
}
}
@Test
public void metricsBeansAreCreated() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class,
ReactiveOAuth2ClientAutoConfiguration.class,
GatewayAutoConfiguration.TokenRelayConfiguration.class))
.withPropertyValues(
"spring.security.oauth2.client.provider[testprovider].authorization-uri=http://localhost",
"spring.security.oauth2.client.provider[testprovider].token-uri=http://localhost/token",
"spring.security.oauth2.client.registration[test].provider=testprovider",
"spring.security.oauth2.client.registration[test].authorization-grant-type=authorization_code",
"spring.security.oauth2.client.registration[test].redirect-uri=http://localhost/redirect",
"spring.security.oauth2.client.registration[test].client-id=login-client")
.run(context -> {
assertThat(context).hasSingleBean(ReactiveOAuth2AuthorizedClientManager.class);
assertThat(context).hasSingleBean(TokenRelayGatewayFilterFactory.class);
});
}
@Test
public void noTokenRelayFilter() {
assertThatThrownBy(() -> {
try (ConfigurableApplicationContext ctx = SpringApplication.run(RouteLocatorBuilderConfig.class,
"--spring.jmx.enabled=false", "--spring.cloud.gateway.filter.token-relay.enabled=false",
"--spring.security.oauth2.client.provider[testprovider].authorization-uri=http://localhost",
"--spring.security.oauth2.client.provider[testprovider].token-uri=http://localhost/token",
"--spring.security.oauth2.client.registration[test].provider=testprovider",
"--spring.security.oauth2.client.registration[test].authorization-grant-type=authorization_code",
"--spring.security.oauth2.client.registration[test].redirect-uri=http://localhost/redirect",
"--spring.security.oauth2.client.registration[test].client-id=login-client", "--server.port=0",
"--spring.cloud.gateway.actuator.verbose.enabled=false")) {
assertThat(ctx.getBeanNamesForType(GatewayLegacyControllerEndpoint.class)).hasSize(1);
}
}).hasRootCauseInstanceOf(IllegalStateException.class)
.hasMessageContaining("No TokenRelayGatewayFilterFactory bean was found. Did you include");
}
@EnableAutoConfiguration
@SpringBootConfiguration
protected static class Config {
}
@EnableAutoConfiguration
@SpringBootConfiguration
protected static class RouteLocatorBuilderConfig {
@Bean
public RouteLocator myRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("test", r -> r.alwaysTrue().filters(GatewayFilterSpec::tokenRelay).uri("http://localhost"))
.build();
}
}
@Configuration
protected static class HttpClientCustomizedConfig {

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.cloud.gateway.security;
package org.springframework.cloud.gateway.filter.factory;
import java.time.Duration;
import java.util.Collections;