From ca81f5671a029fe81d3fe88991d2abf782648310 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Wed, 24 Feb 2016 17:57:52 +0000 Subject: [PATCH] Add HTTP basic back to Eureka client Latest Eureka from Netflix uses a different HTTP client, and it ignores the HTTP basic credentials in a service URL. This change partially restores the old behaviour by providing an interceptor (ClientFilter) that has a single, global username/password taken from the first serviceUrl that contains credentials. Fixes gh-849 --- .../main/asciidoc/spring-cloud-netflix.adoc | 16 +++- .../eureka/EurekaClientAutoConfiguration.java | 78 ++++++++++++++++--- .../MutableDiscoveryClientOptionalArgs.java | 43 ++++++++++ .../EurekaClientAutoConfigurationTests.java | 55 +++++++++++-- 4 files changed, 173 insertions(+), 19 deletions(-) create mode 100644 spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/MutableDiscoveryClientOptionalArgs.java diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index f87cf7a2..87032328 100644 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -74,6 +74,20 @@ ID, or VIP). See {github-code}/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaInstanceConfigBean.java[EurekaInstanceConfigBean] and {github-code}/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientConfigBean.java[EurekaClientConfigBean] for more details of the configurable options. +=== Authenticating with the Eureka Server + +HTTP basic authentication will be automatically added to your eureka +client if one of the `eureka.client.serviceUrl.defaultZone` URLs has +credentials embedded in it (curl style, like +`http://user:password@localhost:8761/eureka`). For more complex needs +you can create a `@Bean` of type `DiscoveryClientOptionalArgs` and +inject `ClientFilter` instances into it, all of which will be applied +to the calls from the client to the server. + +NOTE: Because of a limitation in Eureka it isn't possible to support +per-server basic auth credentials, so only the first set that are +found will be used. + === Status Page and Health Indicator The status page and health indicators for a Eureka instance default to @@ -477,7 +491,7 @@ https://github.com/Netflix/Hystrix/tree/master/hystrix-contrib/hystrix-javanica# for more details. See the https://github.com/Netflix/Hystrix/wiki/Configuration[Hystrix wiki] for details on the properties available. -### Propagating the Security Context or using Spring Scopes +=== Propagating the Security Context or using Spring Scopes If you want some thread local context to propagate into a `@HystrixCommand` the default declaration will not work because it executes the command in a thread pool (in case of timeouts). You can switch Hystrix to use the same thread as the caller using some configuration, or directly in the annotation, by asking it to use a different "Isolation Strategy". For example: diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java index 55d200dc..2919e0e2 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java @@ -21,6 +21,11 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -56,6 +61,8 @@ import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.DiscoveryClient.DiscoveryClientOptionalArgs; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.EurekaClientConfig; +import com.sun.jersey.api.client.filter.ClientFilter; +import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter; import static org.springframework.cloud.util.IdUtils.getDefaultInstanceId; @@ -127,6 +134,12 @@ public class EurekaClientAutoConfiguration { return new EurekaDiscoveryClient(config, client); } + @Bean + @ConditionalOnMissingBean(value = DiscoveryClientOptionalArgs.class, search = SearchStrategy.CURRENT) + public MutableDiscoveryClientOptionalArgs discoveryClientOptionalArgs() { + return new MutableDiscoveryClientOptionalArgs(); + } + @Configuration @ConditionalOnMissingRefreshScope protected static class EurekaClientConfiguration { @@ -134,15 +147,16 @@ public class EurekaClientAutoConfiguration { @Autowired private ApplicationContext context; - @Autowired(required = false) + @Autowired private DiscoveryClientOptionalArgs optionalArgs; @Bean(destroyMethod = "shutdown") @ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT) - public EurekaClient eurekaClient(ApplicationInfoManager applicationInfoManager, + public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config) { - return new CloudEurekaClient(applicationInfoManager, config, - this.optionalArgs, this.context); + DiscoveryClientOptionalArgs args = EurekaClientAutoConfiguration + .getOptionalArgs(config, this.optionalArgs); + return new CloudEurekaClient(manager, config, args, this.context); } @Bean @@ -152,7 +166,6 @@ public class EurekaClientAutoConfiguration { InstanceInfo instanceInfo = new InstanceInfoFactory().create(config); return new ApplicationInfoManager(config, instanceInfo); } - } @Configuration @@ -162,18 +175,19 @@ public class EurekaClientAutoConfiguration { @Autowired private ApplicationContext context; - @Autowired(required = false) + @Autowired private DiscoveryClientOptionalArgs optionalArgs; @Bean(destroyMethod = "shutdown") @ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT) @org.springframework.cloud.context.config.annotation.RefreshScope @Lazy - public EurekaClient eurekaClient(ApplicationInfoManager applicationInfoManager, + public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config, EurekaInstanceConfig instance) { - applicationInfoManager.getInfo(); // force initialization - return new CloudEurekaClient(applicationInfoManager, config, - this.optionalArgs, this.context); + manager.getInfo(); // force initialization + DiscoveryClientOptionalArgs args = EurekaClientAutoConfiguration + .getOptionalArgs(config, this.optionalArgs); + return new CloudEurekaClient(manager, config, args, this.context); } @Bean @@ -233,4 +247,48 @@ public class EurekaClientAutoConfiguration { } + public static DiscoveryClientOptionalArgs getOptionalArgs(EurekaClientConfig config, + DiscoveryClientOptionalArgs optionalArgs) { + Collection filters = new LinkedHashSet<>(); + if (optionalArgs instanceof MutableDiscoveryClientOptionalArgs) { + MutableDiscoveryClientOptionalArgs mutable = (MutableDiscoveryClientOptionalArgs) optionalArgs; + filters = mutable.getAdditionalFilters() != null + ? mutable.getAdditionalFilters() : filters; + ClientFilter filter = getAuthFilter(config); + if (filter != null) { + filters.add(filter); + } + mutable.setAdditionalFilters(filters); + } + return optionalArgs; + } + + private static ClientFilter getAuthFilter(EurekaClientConfig config) { + // Netflix throws away the basic auth credentials from the service URL at runtime, + // so we look at the default zone and try and lift some credentials from there, + // assuming that they don't change from host to host. If they do change from host + // to host user will have to create a custom ClientFilter. + List urls = config + .getEurekaServerServiceUrls(EurekaClientConfigBean.DEFAULT_ZONE); + for (String url : urls) { + try { + String authority = new URI(url).getAuthority(); + authority = authority != null && authority.contains("@") + ? authority.substring(0, authority.indexOf("@")) : null; + if (authority != null) { + String[] values = StringUtils.split(authority, ":"); + if (values == null) { + values = new String[] { authority, "" }; + } + return new HTTPBasicAuthFilter(values[0], values[1]); + } + } + catch (URISyntaxException e) { + // This should not occur + throw new IllegalArgumentException("Cannot parse service URL: " + url, e); + } + } + return null; + } + } diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/MutableDiscoveryClientOptionalArgs.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/MutableDiscoveryClientOptionalArgs.java new file mode 100644 index 00000000..b57eedea --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/MutableDiscoveryClientOptionalArgs.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2015 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.eureka; + +import java.util.Collection; +import java.util.LinkedHashSet; + +import com.netflix.discovery.DiscoveryClient.DiscoveryClientOptionalArgs; +import com.sun.jersey.api.client.filter.ClientFilter; + +/** + * @author Dave Syer + */ +public class MutableDiscoveryClientOptionalArgs extends DiscoveryClientOptionalArgs { + + private Collection additionalFilters; + + @Override + public void setAdditionalFilters(Collection additionalFilters) { + additionalFilters = new LinkedHashSet(additionalFilters); + this.additionalFilters = additionalFilters; + super.setAdditionalFilters(additionalFilters); + } + + public Collection getAdditionalFilters() { + return this.additionalFilters; + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfigurationTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfigurationTests.java index e4f0622f..f55bdecd 100644 --- a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfigurationTests.java +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfigurationTests.java @@ -18,6 +18,8 @@ package org.springframework.cloud.netflix.eureka; import org.junit.After; import org.junit.Test; +import org.mockito.Matchers; +import org.mockito.Mockito; import org.springframework.aop.scope.ScopedProxyFactoryBean; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -25,9 +27,14 @@ import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; import org.springframework.cloud.util.UtilAutoConfiguration; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import com.netflix.discovery.shared.transport.jersey.EurekaJerseyClient; +import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter; +import com.sun.jersey.client.apache4.ApacheHttpClient4; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.boot.test.EnvironmentTestUtils.addEnvironment; @@ -67,8 +74,8 @@ public class EurekaClientAutoConfigurationTests { @Test public void nonSecurePort() { testNonSecurePort("PORT"); - assertEquals("eurekaClient", this.context.getBeanDefinition("eurekaClient") - .getFactoryMethodName()); + assertEquals("eurekaClient", + this.context.getBeanDefinition("eurekaClient").getFactoryMethodName()); } @Test @@ -78,8 +85,8 @@ public class EurekaClientAutoConfigurationTests { setupContext(RefreshAutoConfiguration.class); EurekaInstanceConfigBean instance = this.context .getBean(EurekaInstanceConfigBean.class); - assertTrue("Wrong status page: " + instance.getStatusPageUrl(), instance - .getStatusPageUrl().contains("9999")); + assertTrue("Wrong status page: " + instance.getStatusPageUrl(), + instance.getStatusPageUrl().contains("9999")); } @Test @@ -89,19 +96,28 @@ public class EurekaClientAutoConfigurationTests { setupContext(RefreshAutoConfiguration.class); EurekaInstanceConfigBean instance = this.context .getBean(EurekaInstanceConfigBean.class); - assertTrue("Wrong status page: " + instance.getStatusPageUrl(), instance - .getStatusPageUrl().contains("foo")); + assertTrue("Wrong status page: " + instance.getStatusPageUrl(), + instance.getStatusPageUrl().contains("foo")); } @Test public void refreshScopedBeans() { setupContext(RefreshAutoConfiguration.class); - assertEquals(ScopedProxyFactoryBean.class.getName(), this.context - .getBeanDefinition("eurekaClient").getBeanClassName()); + assertEquals(ScopedProxyFactoryBean.class.getName(), + this.context.getBeanDefinition("eurekaClient").getBeanClassName()); assertEquals(ScopedProxyFactoryBean.class.getName(), this.context .getBeanDefinition("eurekaApplicationInfoManager").getBeanClassName()); } + @Test + public void basicAuth() { + EnvironmentTestUtils.addEnvironment(this.context, "server.port=8989", + "eureka.client.serviceUrl.defaultZone=http://user:foo@example.com:80/eureka"); + setupContext(MockClientConfiguration.class); + ApacheHttpClient4 http = this.context.getBean(ApacheHttpClient4.class); + Mockito.verify(http).addFilter(Matchers.any(HTTPBasicAuthFilter.class)); + } + private void testNonSecurePort(String propName) { addEnvironment(this.context, propName + ":8888"); setupContext(); @@ -118,4 +134,27 @@ public class EurekaClientAutoConfigurationTests { protected static class TestConfiguration { } + + @Configuration + protected static class MockClientConfiguration { + + @Bean + public MutableDiscoveryClientOptionalArgs mutableDiscoveryClientOptionalArgs() { + MutableDiscoveryClientOptionalArgs args = new MutableDiscoveryClientOptionalArgs(); + args.setEurekaJerseyClient(jerseyClient()); + return args; + } + + @Bean + public EurekaJerseyClient jerseyClient() { + EurekaJerseyClient mock = Mockito.mock(EurekaJerseyClient.class); + Mockito.when(mock.getClient()).thenReturn(apacheClient()); + return mock; + } + + @Bean + public ApacheHttpClient4 apacheClient() { + return Mockito.mock(ApacheHttpClient4.class); + } + } }