diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc old mode 100644 new mode 100755 index aabc68b72..84d2d0ebd --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -335,6 +335,16 @@ This section describes how to set up a Eureka server. To include Eureka Server in your project, use the starter with a group ID of `org.springframework.cloud` and an artifact ID of `spring-cloud-starter-netflix-eureka-server`. See the http://projects.spring.io/spring-cloud/[Spring Cloud Project page] for details on setting up your build system with the current Spring Cloud Release Train. +NOTE: If your project already uses Thymeleaf as its template engine, the Freemarker templates of the Eureka server may not be loaded correctly. In this case it is necessary to configure the template loader manually: + +.application.yml +---- +spring: + freemarker: + template-loader-path: classpath:/templates/ + prefer-file-system-access: false +---- + [[spring-cloud-running-eureka-server]] === How to Run a Eureka Server @@ -1505,6 +1515,22 @@ NOTE: This special flag works only with `SimpleHostRoutingFilter`. Also, you loo query parameters with `RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters)`, because the query string is now fetched directly on the original `HttpServletRequest`. +=== Request URI Encoding +When processing the incoming request, request URI is decoded before matching them to routes. +The request URI is then re-encoded when the back end request is rebuilt in the route filters. +This can cause some unexpected behavior if your URI includes the encoded "/" character. + +To use the original request URI, it is possible to pass a special flag to 'ZuulProperties' so that the URI will be taken as is with the `HttpServletRequest::getRequestURI` method, as shown in the following example: + +.application.yml +[source,yaml] +---- + zuul: + decodeUrl: false +---- + +NOTE: If you are overriding request URI using `requestURI` RequestContext attribute and this flag is set to false, then the URL set in the request context will not be encoded. It will be your responsibility to make sure the URL is already encoded. + === Plain Embedded Zuul If you use `@EnableZuulServer` (instead of `@EnableZuulProxy`), you can also run a Zuul server without proxying or selectively switch on parts of the proxying platform. diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/hystrix/security/HystrixSecurityAutoConfigurationTest.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/hystrix/security/HystrixSecurityAutoConfigurationTest.java new file mode 100644 index 000000000..70a78e70d --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/hystrix/security/HystrixSecurityAutoConfigurationTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2013-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 + * + * 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.netflix.hystrix.security; + +import com.netflix.hystrix.strategy.HystrixPlugins; +import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; +import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategyDefault; +import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier; +import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook; +import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher; +import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy; +import org.junit.Test; +import org.mockito.internal.util.reflection.FieldSetter; + +import java.lang.reflect.Field; + +import static org.junit.Assert.assertEquals; + +/** + * @author : ailin.zhou + */ +public class HystrixSecurityAutoConfigurationTest { + + @Test + public void testInit() throws NoSuchFieldException, IllegalAccessException { + + //save test context + HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance() + .getEventNotifier(); + HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance() + .getMetricsPublisher(); + HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance() + .getPropertiesStrategy(); + HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins.getInstance() + .getCommandExecutionHook(); + HystrixConcurrencyStrategy concurrencyStrategy = HystrixPlugins.getInstance().getConcurrencyStrategy(); + + //test + testForMultiConcurrentStrategy(); + + //recover test context + HystrixPlugins.reset(); + HystrixPlugins.getInstance().registerConcurrencyStrategy(concurrencyStrategy); + HystrixPlugins.getInstance().registerEventNotifier(eventNotifier); + HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher); + HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy); + HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook); + + + } + + private void testForMultiConcurrentStrategy() throws IllegalAccessException, NoSuchFieldException { + HystrixSecurityAutoConfiguration securityStrategy = new HystrixSecurityAutoConfiguration(); + + //1.existingConcurrencyStrategy is null, registeredStrategy is default + HystrixPlugins.reset(); + securityStrategy.init(); + //result is default + assertEquals(HystrixConcurrencyStrategyDefault.getInstance(), getOriginalInSecurityConcurrencyStrategy()); + + //2.existingConcurrencyStrategy is null, registered strategy is customized + HystrixPlugins.reset(); + HystrixConcurrencyStrategy customized = new HystrixConcurrencyStrategy() { + }; + HystrixPlugins.getInstance().registerConcurrencyStrategy(customized); + securityStrategy.init(); + //result is customized + assertEquals(customized, getOriginalInSecurityConcurrencyStrategy()); + + //3.existingConcurrencyStrategy is not null, registeredStrategy is default. + HystrixPlugins.reset(); + HystrixConcurrencyStrategy existingConcurrencyStrategy = new HystrixConcurrencyStrategy() { + }; + FieldSetter.setField(securityStrategy, securityStrategy.getClass().getDeclaredField("existingConcurrencyStrategy"), existingConcurrencyStrategy); + securityStrategy.init(); + //result is existingConcurrencyStrategy + assertEquals(existingConcurrencyStrategy, getOriginalInSecurityConcurrencyStrategy()); + + //4.existingConcurrencyStrategy is not null, registeredStrategy is customized. + HystrixPlugins.reset(); + HystrixPlugins.getInstance().registerConcurrencyStrategy(customized); + FieldSetter.setField(securityStrategy, securityStrategy.getClass().getDeclaredField("existingConcurrencyStrategy"), existingConcurrencyStrategy); + securityStrategy.init(); + assertEquals(existingConcurrencyStrategy, getOriginalInSecurityConcurrencyStrategy()); + } + + private HystrixConcurrencyStrategy getOriginalInSecurityConcurrencyStrategy() throws IllegalAccessException, NoSuchFieldException { + HystrixConcurrencyStrategy concurrencyStrategy = HystrixPlugins.getInstance().getConcurrencyStrategy(); + Field existingConcurrencyStrategy = concurrencyStrategy.getClass().getDeclaredField("existingConcurrencyStrategy"); + existingConcurrencyStrategy.setAccessible(true); + HystrixConcurrencyStrategy strategyInSecurityStrategy = (HystrixConcurrencyStrategy) existingConcurrencyStrategy.get(concurrencyStrategy); + return strategyInSecurityStrategy; + } + +} \ No newline at end of file diff --git a/spring-cloud-netflix-hystrix/src/main/java/org/springframework/cloud/netflix/hystrix/security/HystrixSecurityAutoConfiguration.java b/spring-cloud-netflix-hystrix/src/main/java/org/springframework/cloud/netflix/hystrix/security/HystrixSecurityAutoConfiguration.java index 272f234e4..d2eddf08f 100644 --- a/spring-cloud-netflix-hystrix/src/main/java/org/springframework/cloud/netflix/hystrix/security/HystrixSecurityAutoConfiguration.java +++ b/spring-cloud-netflix-hystrix/src/main/java/org/springframework/cloud/netflix/hystrix/security/HystrixSecurityAutoConfiguration.java @@ -18,6 +18,9 @@ package org.springframework.cloud.netflix.hystrix.security; import javax.annotation.PostConstruct; +import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategyDefault; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.AllNestedConditions; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -42,6 +45,7 @@ import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy; @Conditional(HystrixSecurityCondition.class) @ConditionalOnClass({ Hystrix.class, SecurityContext.class }) public class HystrixSecurityAutoConfiguration { + private static final Log LOGGER = LogFactory.getLog(HystrixSecurityAutoConfiguration.class); @Autowired(required = false) private HystrixConcurrencyStrategy existingConcurrencyStrategy; @@ -56,18 +60,36 @@ public class HystrixSecurityAutoConfiguration { .getPropertiesStrategy(); HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins.getInstance() .getCommandExecutionHook(); + HystrixConcurrencyStrategy concurrencyStrategy = detectRegisteredConcurrencyStrategy(); HystrixPlugins.reset(); // Registers existing plugins excepts the Concurrent Strategy plugin. HystrixPlugins.getInstance().registerConcurrencyStrategy( - new SecurityContextConcurrencyStrategy(existingConcurrencyStrategy)); + new SecurityContextConcurrencyStrategy(concurrencyStrategy)); HystrixPlugins.getInstance().registerEventNotifier(eventNotifier); HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher); HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy); HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook); } + private HystrixConcurrencyStrategy detectRegisteredConcurrencyStrategy() { + HystrixConcurrencyStrategy registeredStrategy = HystrixPlugins.getInstance() + .getConcurrencyStrategy(); + if (existingConcurrencyStrategy == null) { + return registeredStrategy; + } + //Hystrix registered a default Strategy. + if (registeredStrategy instanceof HystrixConcurrencyStrategyDefault){ + return existingConcurrencyStrategy; + } + //If registeredStrategy not the default and not some use bean of existingConcurrencyStrategy. + if (!existingConcurrencyStrategy.equals(registeredStrategy)){ + LOGGER.warn("Multiple HystrixConcurrencyStrategy detected. Bean of HystrixConcurrencyStrategy was used."); + } + return existingConcurrencyStrategy; + } + static class HystrixSecurityCondition extends AllNestedConditions { public HystrixSecurityCondition() { diff --git a/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelper.java b/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelper.java old mode 100644 new mode 100755 index 0a5fd7cdb..7197ce98b --- a/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelper.java +++ b/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRequestHelper.java @@ -78,6 +78,8 @@ public class ProxyRequestHelper { private boolean addHostHeader = false; + private boolean urlDecoded = true; + @Deprecated //TODO Remove in 2.1.x public ProxyRequestHelper() {} @@ -86,6 +88,7 @@ public class ProxyRequestHelper { this.ignoredHeaders.addAll(zuulProperties.getIgnoredHeaders()); this.traceRequestBody = zuulProperties.isTraceRequestBody(); this.addHostHeader = zuulProperties.isAddHostHeader(); + this.urlDecoded = zuulProperties.isDecodeUrl(); } public void setWhitelistHosts(Set whitelistHosts) { @@ -114,7 +117,10 @@ public class ProxyRequestHelper { String contextURI = (String) context.get(REQUEST_URI_KEY); if (contextURI != null) { try { - uri = UriUtils.encodePath(contextURI, characterEncoding(request)); + uri = contextURI; + if (this.urlDecoded) { + uri = UriUtils.encodePath(contextURI, characterEncoding(request)); + } } catch (Exception e) { log.debug( diff --git a/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/ZuulProperties.java b/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/ZuulProperties.java old mode 100644 new mode 100755 index c37b39e54..04f7e6fd0 --- a/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/ZuulProperties.java +++ b/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/ZuulProperties.java @@ -140,6 +140,11 @@ public class ZuulProperties { */ private boolean removeSemicolonContent = true; + /** + * Flag to indicate whether to decode the matched URL or use it as is. + */ + private boolean decodeUrl = true; + /** * List of sensitive headers that are not passed to downstream requests. Defaults to a * "safe" set of headers that commonly contain user credentials. It's OK to remove @@ -764,6 +769,14 @@ public class ZuulProperties { this.removeSemicolonContent = removeSemicolonContent; } + public boolean isDecodeUrl() { + return decodeUrl; + } + + public void setDecodeUrl(boolean decodeUrl) { + this.decodeUrl = decodeUrl; + } + public Set getSensitiveHeaders() { return sensitiveHeaders; } diff --git a/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java b/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java old mode 100644 new mode 100755 index 56bcd72db..a1cc941fc --- a/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java +++ b/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java @@ -88,6 +88,7 @@ public class PreDecorationFilter extends ZuulFilter { this.routeLocator = routeLocator; this.properties = properties; this.urlPathHelper.setRemoveSemicolonContent(properties.isRemoveSemicolonContent()); + this.urlPathHelper.setUrlDecode(properties.isDecodeUrl()); this.dispatcherServletPath = dispatcherServletPath; this.proxyRequestHelper = proxyRequestHelper; } diff --git a/spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java b/spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java old mode 100644 new mode 100755 index 4c0074afd..59407f6a3 --- a/spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java +++ b/spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java @@ -330,6 +330,22 @@ public class PreDecorationFilterTests { getHeader(ctx.getOriginResponseHeaders(), "x-zuul-serviceid")); } + @Test + public void dontDecodeUrl() { + this.properties.setPrefix("/api"); + this.properties.setStripPrefix(true); + this.properties.setDecodeUrl(false); + this.request.setRequestURI("/api/foo/encoded%2Fpath"); + this.request.setContextPath("/context-path"); + this.routeLocator.addRoute( + new ZuulRoute("foo", "/foo/**", "foo", null, false, null, null)); + this.filter = new PreDecorationFilter(this.routeLocator, "/", this.properties, + this.proxyRequestHelper); + this.filter.run(); + RequestContext ctx = RequestContext.getCurrentContext(); + assertEquals("/foo/encoded%2Fpath", ctx.get(REQUEST_URI_KEY)); + } + @Test public void routeIgnoreContextPathIfPrefixHeader() { this.properties.setStripPrefix(false);