Commit ca81f567 by Dave Syer

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
parent bc0eef9a
...@@ -74,6 +74,20 @@ ID, or VIP). ...@@ -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. 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 === Status Page and Health Indicator
The status page and health indicators for a Eureka instance default to 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# ...@@ -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 more details. See the https://github.com/Netflix/Hystrix/wiki/Configuration[Hystrix wiki]
for details on the properties available. 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: 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:
......
...@@ -21,6 +21,11 @@ import java.lang.annotation.ElementType; ...@@ -21,6 +21,11 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; 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.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
...@@ -56,6 +61,8 @@ import com.netflix.appinfo.InstanceInfo; ...@@ -56,6 +61,8 @@ import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.DiscoveryClient.DiscoveryClientOptionalArgs; import com.netflix.discovery.DiscoveryClient.DiscoveryClientOptionalArgs;
import com.netflix.discovery.EurekaClient; import com.netflix.discovery.EurekaClient;
import com.netflix.discovery.EurekaClientConfig; 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; import static org.springframework.cloud.util.IdUtils.getDefaultInstanceId;
...@@ -127,6 +134,12 @@ public class EurekaClientAutoConfiguration { ...@@ -127,6 +134,12 @@ public class EurekaClientAutoConfiguration {
return new EurekaDiscoveryClient(config, client); return new EurekaDiscoveryClient(config, client);
} }
@Bean
@ConditionalOnMissingBean(value = DiscoveryClientOptionalArgs.class, search = SearchStrategy.CURRENT)
public MutableDiscoveryClientOptionalArgs discoveryClientOptionalArgs() {
return new MutableDiscoveryClientOptionalArgs();
}
@Configuration @Configuration
@ConditionalOnMissingRefreshScope @ConditionalOnMissingRefreshScope
protected static class EurekaClientConfiguration { protected static class EurekaClientConfiguration {
...@@ -134,15 +147,16 @@ public class EurekaClientAutoConfiguration { ...@@ -134,15 +147,16 @@ public class EurekaClientAutoConfiguration {
@Autowired @Autowired
private ApplicationContext context; private ApplicationContext context;
@Autowired(required = false) @Autowired
private DiscoveryClientOptionalArgs optionalArgs; private DiscoveryClientOptionalArgs optionalArgs;
@Bean(destroyMethod = "shutdown") @Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT) @ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
public EurekaClient eurekaClient(ApplicationInfoManager applicationInfoManager, public EurekaClient eurekaClient(ApplicationInfoManager manager,
EurekaClientConfig config) { EurekaClientConfig config) {
return new CloudEurekaClient(applicationInfoManager, config, DiscoveryClientOptionalArgs args = EurekaClientAutoConfiguration
this.optionalArgs, this.context); .getOptionalArgs(config, this.optionalArgs);
return new CloudEurekaClient(manager, config, args, this.context);
} }
@Bean @Bean
...@@ -152,7 +166,6 @@ public class EurekaClientAutoConfiguration { ...@@ -152,7 +166,6 @@ public class EurekaClientAutoConfiguration {
InstanceInfo instanceInfo = new InstanceInfoFactory().create(config); InstanceInfo instanceInfo = new InstanceInfoFactory().create(config);
return new ApplicationInfoManager(config, instanceInfo); return new ApplicationInfoManager(config, instanceInfo);
} }
} }
@Configuration @Configuration
...@@ -162,18 +175,19 @@ public class EurekaClientAutoConfiguration { ...@@ -162,18 +175,19 @@ public class EurekaClientAutoConfiguration {
@Autowired @Autowired
private ApplicationContext context; private ApplicationContext context;
@Autowired(required = false) @Autowired
private DiscoveryClientOptionalArgs optionalArgs; private DiscoveryClientOptionalArgs optionalArgs;
@Bean(destroyMethod = "shutdown") @Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT) @ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
@org.springframework.cloud.context.config.annotation.RefreshScope @org.springframework.cloud.context.config.annotation.RefreshScope
@Lazy @Lazy
public EurekaClient eurekaClient(ApplicationInfoManager applicationInfoManager, public EurekaClient eurekaClient(ApplicationInfoManager manager,
EurekaClientConfig config, EurekaInstanceConfig instance) { EurekaClientConfig config, EurekaInstanceConfig instance) {
applicationInfoManager.getInfo(); // force initialization manager.getInfo(); // force initialization
return new CloudEurekaClient(applicationInfoManager, config, DiscoveryClientOptionalArgs args = EurekaClientAutoConfiguration
this.optionalArgs, this.context); .getOptionalArgs(config, this.optionalArgs);
return new CloudEurekaClient(manager, config, args, this.context);
} }
@Bean @Bean
...@@ -233,4 +247,48 @@ public class EurekaClientAutoConfiguration { ...@@ -233,4 +247,48 @@ public class EurekaClientAutoConfiguration {
} }
public static DiscoveryClientOptionalArgs getOptionalArgs(EurekaClientConfig config,
DiscoveryClientOptionalArgs optionalArgs) {
Collection<ClientFilter> 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<String> 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;
}
} }
/*
* 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<ClientFilter> additionalFilters;
@Override
public void setAdditionalFilters(Collection<ClientFilter> additionalFilters) {
additionalFilters = new LinkedHashSet<ClientFilter>(additionalFilters);
this.additionalFilters = additionalFilters;
super.setAdditionalFilters(additionalFilters);
}
public Collection<ClientFilter> getAdditionalFilters() {
return this.additionalFilters;
}
}
...@@ -18,6 +18,8 @@ package org.springframework.cloud.netflix.eureka; ...@@ -18,6 +18,8 @@ package org.springframework.cloud.netflix.eureka;
import org.junit.After; import org.junit.After;
import org.junit.Test; import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mockito;
import org.springframework.aop.scope.ScopedProxyFactoryBean; import org.springframework.aop.scope.ScopedProxyFactoryBean;
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
...@@ -25,9 +27,14 @@ import org.springframework.boot.test.EnvironmentTestUtils; ...@@ -25,9 +27,14 @@ import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration; import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
import org.springframework.cloud.util.UtilAutoConfiguration; import org.springframework.cloud.util.UtilAutoConfiguration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; 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.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.springframework.boot.test.EnvironmentTestUtils.addEnvironment; import static org.springframework.boot.test.EnvironmentTestUtils.addEnvironment;
...@@ -67,8 +74,8 @@ public class EurekaClientAutoConfigurationTests { ...@@ -67,8 +74,8 @@ public class EurekaClientAutoConfigurationTests {
@Test @Test
public void nonSecurePort() { public void nonSecurePort() {
testNonSecurePort("PORT"); testNonSecurePort("PORT");
assertEquals("eurekaClient", this.context.getBeanDefinition("eurekaClient") assertEquals("eurekaClient",
.getFactoryMethodName()); this.context.getBeanDefinition("eurekaClient").getFactoryMethodName());
} }
@Test @Test
...@@ -78,8 +85,8 @@ public class EurekaClientAutoConfigurationTests { ...@@ -78,8 +85,8 @@ public class EurekaClientAutoConfigurationTests {
setupContext(RefreshAutoConfiguration.class); setupContext(RefreshAutoConfiguration.class);
EurekaInstanceConfigBean instance = this.context EurekaInstanceConfigBean instance = this.context
.getBean(EurekaInstanceConfigBean.class); .getBean(EurekaInstanceConfigBean.class);
assertTrue("Wrong status page: " + instance.getStatusPageUrl(), instance assertTrue("Wrong status page: " + instance.getStatusPageUrl(),
.getStatusPageUrl().contains("9999")); instance.getStatusPageUrl().contains("9999"));
} }
@Test @Test
...@@ -89,19 +96,28 @@ public class EurekaClientAutoConfigurationTests { ...@@ -89,19 +96,28 @@ public class EurekaClientAutoConfigurationTests {
setupContext(RefreshAutoConfiguration.class); setupContext(RefreshAutoConfiguration.class);
EurekaInstanceConfigBean instance = this.context EurekaInstanceConfigBean instance = this.context
.getBean(EurekaInstanceConfigBean.class); .getBean(EurekaInstanceConfigBean.class);
assertTrue("Wrong status page: " + instance.getStatusPageUrl(), instance assertTrue("Wrong status page: " + instance.getStatusPageUrl(),
.getStatusPageUrl().contains("foo")); instance.getStatusPageUrl().contains("foo"));
} }
@Test @Test
public void refreshScopedBeans() { public void refreshScopedBeans() {
setupContext(RefreshAutoConfiguration.class); setupContext(RefreshAutoConfiguration.class);
assertEquals(ScopedProxyFactoryBean.class.getName(), this.context assertEquals(ScopedProxyFactoryBean.class.getName(),
.getBeanDefinition("eurekaClient").getBeanClassName()); this.context.getBeanDefinition("eurekaClient").getBeanClassName());
assertEquals(ScopedProxyFactoryBean.class.getName(), this.context assertEquals(ScopedProxyFactoryBean.class.getName(), this.context
.getBeanDefinition("eurekaApplicationInfoManager").getBeanClassName()); .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) { private void testNonSecurePort(String propName) {
addEnvironment(this.context, propName + ":8888"); addEnvironment(this.context, propName + ":8888");
setupContext(); setupContext();
...@@ -118,4 +134,27 @@ public class EurekaClientAutoConfigurationTests { ...@@ -118,4 +134,27 @@ public class EurekaClientAutoConfigurationTests {
protected static class TestConfiguration { 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);
}
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment