initial per Route filter mechanism
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
package org.springframework.cloud.gateway.config;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.validation.ValidationException;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import static org.springframework.util.StringUtils.tokenizeToStringArray;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class FilterDefinition {
|
||||
@NotNull
|
||||
private String name;
|
||||
@NotNull
|
||||
private String value;
|
||||
|
||||
private String[] args;
|
||||
|
||||
public FilterDefinition() {
|
||||
}
|
||||
|
||||
public FilterDefinition(String text) {
|
||||
int eqIdx = text.indexOf("=");
|
||||
if (eqIdx <= 0) {
|
||||
throw new ValidationException("Unable to parse FilterDefinition text '" + text + "'" +
|
||||
", must be of the form name=value");
|
||||
}
|
||||
setName(text.substring(0, eqIdx));
|
||||
|
||||
String[] args = tokenizeToStringArray(text.substring(eqIdx+1), ",");
|
||||
|
||||
setValue(args[0]);
|
||||
|
||||
if (args.length > 1) {
|
||||
setArgs(Arrays.copyOfRange(args, 1, args.length));
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String[] getArgs() {
|
||||
return args;
|
||||
}
|
||||
|
||||
public void setArgs(String[] args) {
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
FilterDefinition that = (FilterDefinition) o;
|
||||
return Objects.equals(name, that.name) &&
|
||||
Objects.equals(value, that.value) &&
|
||||
Arrays.equals(args, that.args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, value, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("FilterDefinition{");
|
||||
sb.append("name='").append(name).append('\'');
|
||||
sb.append(", value='").append(value).append('\'');
|
||||
sb.append(", args=").append(Arrays.toString(args));
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.cloud.gateway.actuate.GatewayEndpoint;
|
||||
import org.springframework.cloud.gateway.api.RouteReader;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.definition.AppendRequestHeaderFilter;
|
||||
import org.springframework.cloud.gateway.filter.definition.AppendResponseHeaderFilter;
|
||||
import org.springframework.cloud.gateway.filter.definition.GatewayFilterDefinition;
|
||||
import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter;
|
||||
import org.springframework.cloud.gateway.handler.GatewayFilteringWebHandler;
|
||||
import org.springframework.cloud.gateway.handler.GatewayPredicateHandlerMapping;
|
||||
@@ -60,10 +63,14 @@ public class GatewayAutoConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GatewayFilteringWebHandler gatewayFilteringWebHandler(GatewayWebHandler gatewayWebHandler, GatewayFilter[] filters) {
|
||||
return new GatewayFilteringWebHandler(gatewayWebHandler, filters);
|
||||
public GatewayFilteringWebHandler gatewayFilteringWebHandler(GatewayWebHandler gatewayWebHandler,
|
||||
List<GatewayFilter> filters,
|
||||
List<GatewayFilterDefinition> filterDefinitions) {
|
||||
return new GatewayFilteringWebHandler(gatewayWebHandler, filters, filterDefinitions);
|
||||
}
|
||||
|
||||
// Predicate beans
|
||||
|
||||
@Bean
|
||||
public GatewayPredicateHandlerMapping gatewayPredicateHandlerMapping(GatewayFilteringWebHandler webHandler,
|
||||
List<GatewayPredicate> predicates,
|
||||
@@ -101,6 +108,18 @@ public class GatewayAutoConfiguration {
|
||||
return new UrlPredicate();
|
||||
}
|
||||
|
||||
// Filter beans
|
||||
|
||||
@Bean
|
||||
public AppendRequestHeaderFilter appendRequestHeaderFilter() {
|
||||
return new AppendRequestHeaderFilter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AppendResponseHeaderFilter appendResponseHeaderFilter() {
|
||||
return new AppendResponseHeaderFilter();
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnClass(Endpoint.class)
|
||||
protected static class GatewayActuatorConfiguration {
|
||||
|
||||
@@ -24,6 +24,9 @@ public class Route {
|
||||
@Valid
|
||||
private List<PredicateDefinition> predicates = new ArrayList<>();
|
||||
|
||||
@Valid
|
||||
private List<FilterDefinition> filters = new ArrayList<>();
|
||||
|
||||
@NotNull
|
||||
private URI uri;
|
||||
|
||||
@@ -63,6 +66,14 @@ public class Route {
|
||||
this.predicates = predicates;
|
||||
}
|
||||
|
||||
public List<FilterDefinition> getFilters() {
|
||||
return filters;
|
||||
}
|
||||
|
||||
public void setFilters(List<FilterDefinition> filters) {
|
||||
this.filters = filters;
|
||||
}
|
||||
|
||||
public URI getUri() {
|
||||
return uri;
|
||||
}
|
||||
@@ -91,6 +102,7 @@ public class Route {
|
||||
return "Route{" +
|
||||
"id='" + id + '\'' +
|
||||
", predicates=" + predicates +
|
||||
", filters=" + filters +
|
||||
", uri=" + uri +
|
||||
'}';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.springframework.cloud.gateway.filter.definition;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class AppendRequestHeaderFilter implements GatewayFilterDefinition {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "AppendRequestHeader";
|
||||
}
|
||||
|
||||
@Override
|
||||
public GatewayFilter apply(String header, String[] args) {
|
||||
Assert.isTrue(args != null && args.length == 1,
|
||||
"args must have one entry");
|
||||
|
||||
//TODO: caching can happen here
|
||||
return (exchange, chain) -> {
|
||||
ServerHttpRequest request = exchange.getRequest().mutate()
|
||||
.header(header, args[0])
|
||||
.build();
|
||||
|
||||
return chain.filter(exchange.mutate().request(request).build());
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.springframework.cloud.gateway.filter.definition;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class AppendResponseHeaderFilter implements GatewayFilterDefinition {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "AppendResponseHeader";
|
||||
}
|
||||
|
||||
@Override
|
||||
public GatewayFilter apply(String header, String[] args) {
|
||||
Assert.isTrue(args != null && args.length == 1,
|
||||
"args must have one entry");
|
||||
|
||||
//TODO: caching can happen here
|
||||
return (exchange, chain) -> {
|
||||
exchange.getResponse().getHeaders().add(header, args[0]);
|
||||
|
||||
return chain.filter(exchange);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.springframework.cloud.gateway.filter.definition;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public interface GatewayFilterDefinition {
|
||||
|
||||
String getName();
|
||||
|
||||
GatewayFilter apply(String value, String[] args);
|
||||
}
|
||||
@@ -16,11 +16,21 @@
|
||||
|
||||
package org.springframework.cloud.gateway.handler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.cloud.gateway.config.Route;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.definition.GatewayFilterDefinition;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import org.springframework.web.server.WebHandler;
|
||||
@@ -28,6 +38,9 @@ import org.springframework.web.server.handler.WebHandlerDecorator;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.springframework.cloud.gateway.filter.GatewayFilter.GATEWAY_ROUTE_ATTR;
|
||||
|
||||
/**
|
||||
* WebHandler that delegates to a chain of {@link GatewayFilter} instances and then
|
||||
* to the target {@link WebHandler}.
|
||||
@@ -36,20 +49,22 @@ import reactor.core.publisher.Mono;
|
||||
* @since 5.0
|
||||
*/
|
||||
public class GatewayFilteringWebHandler extends WebHandlerDecorator {
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final List<GatewayFilter> filters;
|
||||
private final Map<String, GatewayFilterDefinition> filterDefinitions = new HashMap<>();
|
||||
|
||||
|
||||
public GatewayFilteringWebHandler(WebHandler targetHandler, GatewayFilter... filters) {
|
||||
public GatewayFilteringWebHandler(WebHandler targetHandler, List<GatewayFilter> filters,
|
||||
List<GatewayFilterDefinition> filterDefinitions) {
|
||||
super(targetHandler);
|
||||
this.filters = initList(filters);
|
||||
initList(filterDefinitions).forEach(def -> this.filterDefinitions.put(def.getName(), def));
|
||||
}
|
||||
|
||||
private static List<GatewayFilter> initList(GatewayFilter[] list) {
|
||||
return (list != null ? Collections.unmodifiableList(Arrays.asList(list)) : Collections.emptyList());
|
||||
private static <T> List<T> initList(List<T> list) {
|
||||
return (list != null ? list : emptyList());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a read-only list of the configured filters.
|
||||
*/
|
||||
@@ -59,13 +74,48 @@ public class GatewayFilteringWebHandler extends WebHandlerDecorator {
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange) {
|
||||
return new DefaultWebFilterChain().filter(exchange);
|
||||
//TODO: probably a java 8 stream way of doing this
|
||||
ArrayList<GatewayFilter> routeFilters = new ArrayList<>(this.filters);
|
||||
Optional<Route> route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
|
||||
if (route.isPresent() && !route.get().getFilters().isEmpty()) {
|
||||
routeFilters.addAll(loadFilters(route.get()));
|
||||
}
|
||||
return new DefaultWebFilterChain(routeFilters, getDelegate()).filter(exchange);
|
||||
}
|
||||
|
||||
private Collection<GatewayFilter> loadFilters(Route route) {
|
||||
return route.getFilters().stream()
|
||||
.map(definition -> {
|
||||
GatewayFilterDefinition filter = this.filterDefinitions.get(definition.getName());
|
||||
if (filter == null) {
|
||||
throw new IllegalArgumentException("Unable to find GatewayFilterDefinition with name " + definition.getName());
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
List<String> args;
|
||||
if (definition.getArgs() != null) {
|
||||
args = Arrays.asList(definition.getArgs());
|
||||
} else {
|
||||
args = Collections.emptyList();
|
||||
}
|
||||
logger.debug("Route " + route.getId() + " applying filter "+ definition.getValue()
|
||||
+ ", " + args + " to " + definition.getName());
|
||||
}
|
||||
return filter.apply(definition.getValue(), definition.getArgs());
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
private class DefaultWebFilterChain implements WebFilterChain {
|
||||
private static class DefaultWebFilterChain implements WebFilterChain {
|
||||
|
||||
private int index;
|
||||
private final List<GatewayFilter> filters;
|
||||
private final WebHandler delegate;
|
||||
|
||||
public DefaultWebFilterChain(List<GatewayFilter> filters, WebHandler delegate) {
|
||||
this.filters = filters;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange) {
|
||||
@@ -74,7 +124,7 @@ public class GatewayFilteringWebHandler extends WebHandlerDecorator {
|
||||
return filter.filter(exchange, this);
|
||||
}
|
||||
else {
|
||||
return getDelegate().handle(exchange);
|
||||
return this.delegate.handle(exchange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,25 @@ public class GatewayIntegrationTests {
|
||||
.verify(Duration.ofSeconds(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appendRequestHeaderFilterWorks() {
|
||||
Mono<Map> result = webClient.exchange(
|
||||
GET("http://localhost:" + port + "/headers")
|
||||
.header("Host", "www.bar.org")
|
||||
.build()
|
||||
).then(response -> response.body(toMono(Map.class)));
|
||||
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(
|
||||
response -> {
|
||||
assertThat(response).containsKey("headers").isInstanceOf(Map.class);
|
||||
Map<String, Object> headers = (Map<String, Object>) response.get("headers");
|
||||
assertThat(headers).containsEntry("X-Request-Foo", "Bar");
|
||||
})
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void postWorks() {
|
||||
ClientRequest<Mono<String>> request = POST("http://localhost:" + port + "/post")
|
||||
@@ -162,6 +181,8 @@ public class GatewayIntegrationTests {
|
||||
.isEqualTo(GatewayPredicateHandlerMapping.class.getSimpleName());
|
||||
assertThat(httpHeaders.getFirst(ROUTE_ID_HEADER))
|
||||
.isEqualTo("host_foo_path_headers_to_httpbin");
|
||||
assertThat(httpHeaders.getFirst("X-Response-Foo"))
|
||||
.isEqualTo("Bar");
|
||||
assertThat(statusCode).isEqualTo(HttpStatus.OK);
|
||||
})
|
||||
.expectComplete()
|
||||
|
||||
@@ -17,6 +17,18 @@ spring:
|
||||
- Query=foo, ba.
|
||||
- Query=baz
|
||||
- Cookie=chocolate, ch.p
|
||||
filters:
|
||||
- AppendRequestHeader=X-Request-Foo, Bar
|
||||
- AppendResponseHeader=X-Response-Foo, Bar
|
||||
|
||||
# =====================================
|
||||
- id: host_bar_path_headers_to_httpbin
|
||||
uri: http://httpbin.org:80
|
||||
predicates:
|
||||
- Host=**.bar.org
|
||||
- Url=/headers
|
||||
filters:
|
||||
- AppendRequestHeader=X-Request-Foo, Bar
|
||||
|
||||
# =====================================
|
||||
- id: default_path_to_httpbin
|
||||
@@ -30,4 +42,4 @@ logging:
|
||||
|
||||
management:
|
||||
context-path: /admin
|
||||
# port: 8081
|
||||
port: 8081
|
||||
|
||||
Reference in New Issue
Block a user