Add support for forward:/// uri's.

Let's spring's `DispatcherHandler` handle forwarded requests.

fixes gh-78
This commit is contained in:
Spencer Gibb
2017-10-06 14:10:59 -04:00
parent 36e88a3b2b
commit a9d73bb0a7
8 changed files with 174 additions and 4 deletions

View File

@@ -582,9 +582,17 @@ In either case, the HTTP status of the response will be set to 401.
The `GlobalFilter` interface has the same signature as `WebFilter`. These are special filters that are conditionally applied to all routes. (This interface and usage are subject to change in future milestones).
=== Combined Global Filter and WebFilter Ordering
TODO: document ordering
=== Forward Routing Filter
The `ForwardRoutingFilter` looks for a URI in the exchange attribute `ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR`. If the url has a `forward` scheme (ie `forward:///localendpoint`), it will use the Spring `DispatcherHandler` to handler the request. The unmodified original url is appended to the list in the `ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR` attribute.
=== LoadBalancerClient Filter
The `LoadBalancerClientFilter` looks for a URI in the exchange attribute `ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR`. If the url has a `lb` scheme (ie `lb://myservice`), it will use the Spring Cloud `LoadBalancerClient` to resolve the name (`myservice` in the previous example) to an actual host and port and replace the URI in the same attribute. The unmodified original url is placed in the `ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR` attribute.
The `LoadBalancerClientFilter` looks for a URI in the exchange attribute `ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR`. If the url has a `lb` scheme (ie `lb://myservice`), it will use the Spring Cloud `LoadBalancerClient` to resolve the name (`myservice` in the previous example) to an actual host and port and replace the URI in the same attribute. The unmodified original url is appended to the list in the `ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR` attribute.
=== Netty Routing Filter

View File

@@ -31,6 +31,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.actuate.GatewayEndpoint;
import org.springframework.cloud.gateway.filter.ForwardRoutingFilter;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyRoutingFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
@@ -81,6 +82,7 @@ import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
import org.springframework.web.reactive.socket.client.WebSocketClient;
import org.springframework.web.reactive.socket.server.WebSocketService;
@@ -199,6 +201,12 @@ public class GatewayAutoConfiguration {
return new RouteToRequestUrlFilter();
}
@Bean
@ConditionalOnBean(DispatcherHandler.class)
public ForwardRoutingFilter forwardRoutingFilter(DispatcherHandler dispatcherHandler) {
return new ForwardRoutingFilter(dispatcherHandler);
}
@Bean
public WebSocketService webSocketService() {
return new HandshakeWebSocketService();

View File

@@ -0,0 +1,50 @@
package org.springframework.cloud.gateway.filter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.Ordered;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.net.URI;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.isAlreadyRouted;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.setAlreadyRouted;
public class ForwardRoutingFilter implements GlobalFilter, Ordered {
private static final Log log = LogFactory.getLog(ForwardRoutingFilter.class);
private final DispatcherHandler dispatcherHandler;
public ForwardRoutingFilter(DispatcherHandler dispatcherHandler) {
this.dispatcherHandler = dispatcherHandler;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
String scheme = requestUrl.getScheme();
if (isAlreadyRouted(exchange) || !scheme.equals("forward")) {
return chain.filter(exchange);
}
setAlreadyRouted(exchange);
//TODO: translate url?
if (log.isTraceEnabled()) {
log.trace("Forwarding to URI: "+requestUrl);
}
return this.dispatcherHandler.handle(exchange);
}
}

View File

@@ -17,18 +17,25 @@
package org.springframework.cloud.gateway.filter.factory;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.tuple.Tuple;
import org.springframework.web.server.WebFilter;
import java.util.Arrays;
import java.util.List;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl;
/**
* @author Spencer Gibb
*/
public class PrefixPathWebFilterFactory implements WebFilterFactory {
private static final Log log = LogFactory.getLog(PrefixPathWebFilterFactory.class);
public static final String PREFIX_KEY = "prefix";
@Override
@@ -43,12 +50,19 @@ public class PrefixPathWebFilterFactory implements WebFilterFactory {
return (exchange, chain) -> {
ServerHttpRequest req = exchange.getRequest();
addOriginalRequestUrl(exchange, req.getURI());
String newPath = prefix + req.getURI().getPath();
ServerHttpRequest request = req.mutate()
.path(newPath)
.build();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, request.getURI());
if (log.isTraceEnabled()) {
log.trace("Prefixed URI with: "+prefix+" -> "+request.getURI());
}
return chain.filter(exchange.mutate().request(request).build());
};
}

View File

@@ -29,6 +29,7 @@ import org.springframework.web.server.WebFilter;
import org.springframework.web.util.UriTemplate;
import org.springframework.web.util.pattern.PathPattern.PathMatchInfo;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl;
@@ -66,6 +67,8 @@ public class SetPathWebFilterFactory implements WebFilterFactory {
URI uri = uriTemplate.expand(uriVariables);
String newPath = uri.getPath();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri);
ServerHttpRequest request = req.mutate()
.path(newPath)
.build();

View File

@@ -37,7 +37,7 @@ public class ServerWebExchangeUtils {
public static final String CLIENT_RESPONSE_ATTR = qualify("webHandlerClientResponse");
public static final String GATEWAY_ROUTE_ATTR = qualify("gatewayRoute");
public static final String GATEWAY_REQUEST_URL_ATTR = qualify("gatewayRequestUrl");
public static final String GATEWAY_ORIGINAL_REQUEST_URL_ATTR = qualify("gatewayOribinalRequestUrl");
public static final String GATEWAY_ORIGINAL_REQUEST_URL_ATTR = qualify("gatewayOriginalRequestUrl");
public static final String GATEWAY_HANDLER_MAPPER_ATTR = qualify("gatewayHandlerMapper");
/**

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2013-2017 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
*
* http://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.test;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.Map;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@DirtiesContext
@SuppressWarnings("unchecked")
public class ForwardTests {
@LocalServerPort
protected int port = 0;
protected WebTestClient client;
@Before
public void setup() {
String baseUri = "http://localhost:" + port;
this.client = WebTestClient.bindToServer()
.baseUrl(baseUri)
.build();
}
@Test
public void forwardWorks() {
this.client
.get()
.uri("/localcontroller")
.header(HttpHeaders.HOST, "www.forward.org")
.exchange()
.expectStatus().isOk()
.expectBody().json("{\"from\":\"localcontroller\"}");
}
@EnableAutoConfiguration
@SpringBootConfiguration
@RestController
@Import(PermitAllSecurityConfiguration.class)
public static class TestConfig {
@RequestMapping("/httpbin/localcontroller")
public Map<String, String> localController() {
return Collections.singletonMap("from", "localcontroller");
}
}
}

View File

@@ -58,6 +58,12 @@ spring:
filters:
- AddResponseHeader=X-Request-Foo, Bar
# =====================================
- id: forward_test
uri: forward:///localcontroller
predicates:
- Host=**.forward.org
# =====================================
- id: hystrix_failure_test
uri: ${test.uri}