diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 450aa02d..4b8d77de 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -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 diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 7fd53c0f..ff5fc728 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -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(); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilter.java new file mode 100644 index 00000000..544f4394 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilter.java @@ -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 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); + } +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/PrefixPathWebFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/PrefixPathWebFilterFactory.java index 6cec28da..a9f7f7aa 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/PrefixPathWebFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/PrefixPathWebFilterFactory.java @@ -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()); }; } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SetPathWebFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SetPathWebFilterFactory.java index c287bda4..1d5f02c9 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SetPathWebFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/SetPathWebFilterFactory.java @@ -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(); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java index d1d8cdb3..f1913936 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java @@ -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"); /** diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ForwardTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ForwardTests.java new file mode 100644 index 00000000..e4fcfc6e --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ForwardTests.java @@ -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 localController() { + return Collections.singletonMap("from", "localcontroller"); + } + } + +} diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index bb9c1b8c..41654845 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -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}