Skip to content

Commit

Permalink
OAuth2 auto configuration support for Eureka Client.
Browse files Browse the repository at this point in the history
  • Loading branch information
daniellavoie committed Dec 19, 2017
1 parent 06398da commit 9ced12a
Show file tree
Hide file tree
Showing 20 changed files with 341 additions and 64 deletions.
29 changes: 25 additions & 4 deletions docs/src/main/asciidoc/spring-cloud-netflix.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,35 @@ See {github-code}/spring-cloud-netflix-eureka-client/src/main/java/org/springfra

To disable the Eureka Discovery Client you can set `eureka.client.enabled` to `false`.

=== Authenticating with the Eureka Server
=== Basic authentication 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
`http://user:password@localhost:8761/eureka`).

=== OAuth2 client support

OAuth 2 support will be auto configured when `org.springframework.security.oauth:spring-security-oauth2`
is available in your classpath and you provide a `@Bean` of type `EurekaOAuth2ResourceDetails` within
your context. OAuth2 resource details can by configured with the following properties:

.application.yml
----
eureka:
client:
oauth2:
client_secret: client-secret
client_id: user
access_token_uri: oauth2-token-uri
----

IMPORTANT: OAuth2 client support is only supported with Rest Template. Jersey dependencies must be excluded. See <<EurekaClient without Jersey>> for more information.

=== Custom authentication

For more complex needs you can create a `@Bean` of type `DiscoveryClientOptionalArgs` and
inject `ClientFilter` or `RestTemplace` 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
Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<spring-cloud-commons.version>2.0.0.BUILD-SNAPSHOT</spring-cloud-commons.version>
<spring-cloud-config.version>2.0.0.BUILD-SNAPSHOT</spring-cloud-config.version>
<spring-cloud-stream.version>Elmhurst.BUILD-SNAPSHOT</spring-cloud-stream.version>
<spring-security-oauth2.version>2.2.1.RELEASE</spring-security-oauth2.version>
<!-- Has to be a stable version (not one that depends on this version of netflix): -->
<donotreplacespring-cloud-contract.version>1.2.0.RELEASE</donotreplacespring-cloud-contract.version>

Expand Down Expand Up @@ -119,6 +120,11 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>${spring-security-oauth2.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-http</artifactId>
Expand Down
6 changes: 6 additions & 0 deletions spring-cloud-netflix-eureka-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@
<artifactId>ribbon-httpclient</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@

package org.springframework.cloud.netflix.eureka.config;

import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.cloud.netflix.eureka.MutableDiscoveryClientOptionalArgs;
import org.springframework.cloud.netflix.eureka.http.BasicEurekaRestTemplateFactory;
import org.springframework.cloud.netflix.eureka.http.EurekaRestTemplateFactory;
import org.springframework.cloud.netflix.eureka.http.RestTemplateDiscoveryClientOptionalArgs;
import org.springframework.cloud.netflix.eureka.http.RestTemplateTransportClientFactories;
import org.springframework.cloud.netflix.eureka.http.RestTemplateTransportClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

Expand All @@ -31,12 +36,37 @@
* @author Daniel Lavoie
*/
@Configuration
@AutoConfigureAfter(EurekaOAuth2AutoConfiguration.class)
public class DiscoveryClientOptionalArgsConfiguration {

@Bean
@ConditionalOnMissingBean(value = EurekaRestTemplateFactory.class)
public EurekaRestTemplateFactory eurekaRestTemplateFactory() {
return new BasicEurekaRestTemplateFactory();
}

@Bean
@ConditionalOnMissingBean
public RestTemplateTransportClientFactory restTemplateTransportClientFactory(
EurekaRestTemplateFactory eurekaRestTemplateFactory) {
return new RestTemplateTransportClientFactory(eurekaRestTemplateFactory);
}

@Bean
@ConditionalOnMissingBean
public RestTemplateTransportClientFactories restTemplateTransportClientFactories(
RestTemplateTransportClientFactory restTemplateTransportClientFactory) {
return new RestTemplateTransportClientFactories(
restTemplateTransportClientFactory);
}

@Bean
@ConditionalOnMissingClass("com.sun.jersey.api.client.filter.ClientFilter")
@ConditionalOnMissingBean(value = AbstractDiscoveryClientOptionalArgs.class, search = SearchStrategy.CURRENT)
public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOptionalArgs() {
return new RestTemplateDiscoveryClientOptionalArgs();
public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOptionalArgs(
RestTemplateTransportClientFactories restTemplateTransportClientFactories) {
return new RestTemplateDiscoveryClientOptionalArgs(
restTemplateTransportClientFactories);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.springframework.cloud.netflix.eureka.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.netflix.eureka.http.EurekaRestTemplateFactory;
import org.springframework.cloud.netflix.eureka.http.oauth2.EurekaOAuth2ResourceDetails;
import org.springframework.cloud.netflix.eureka.http.oauth2.OAuth2EurekaRestTemplateFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails;

@Configuration
@ConditionalOnClass(BaseOAuth2ProtectedResourceDetails.class)
public class EurekaOAuth2AutoConfiguration {
@Bean
@ConditionalOnProperty("eureka.client.oauth2.client-id")
public EurekaOAuth2ResourceDetails eurekaOAuth2ResourceDetails() {
return new EurekaOAuth2ResourceDetails();
}

@Bean
@ConditionalOnBean(EurekaOAuth2ResourceDetails.class)
public EurekaRestTemplateFactory eurekaRestTemplateFactory(
EurekaOAuth2ResourceDetails eurekaOAuth2ResourceDetails) {
return new OAuth2EurekaRestTemplateFactory(eurekaOAuth2ResourceDetails);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 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.netflix.eureka.http;

import java.net.URI;
import java.net.URISyntaxException;

import org.springframework.http.client.support.BasicAuthorizationInterceptor;
import org.springframework.web.client.RestTemplate;

/**
* @author Daniel Lavoie
*/
public class BasicEurekaRestTemplateFactory implements EurekaRestTemplateFactory {
@Override
public RestTemplate newRestTemplate(String serviceUrl) {
RestTemplate restTemplate = new RestTemplate();
try {
URI serviceURI = new URI(serviceUrl);
if (serviceURI.getUserInfo() != null) {
String[] credentials = serviceURI.getUserInfo().split(":");
if (credentials.length == 2) {
restTemplate.getInterceptors().add(new BasicAuthorizationInterceptor(
credentials[0], credentials[1]));
}
}
}
catch (URISyntaxException ignore) {

}

return restTemplate;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 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.netflix.eureka.http;

import org.springframework.web.client.RestTemplate;

/**
* @author Daniel Lavoie
*/
public interface EurekaRestTemplateFactory {
RestTemplate newRestTemplate(String serviceUrl);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@
import com.netflix.discovery.AbstractDiscoveryClientOptionalArgs;

/**
* Eureka client extension that allows customization of the transport client.
*
* @author Daniel Lavoie
*/
public class RestTemplateDiscoveryClientOptionalArgs
extends AbstractDiscoveryClientOptionalArgs<Void> {
public RestTemplateDiscoveryClientOptionalArgs() {
setTransportClientFactories(new RestTemplateTransportClientFactories());
public RestTemplateDiscoveryClientOptionalArgs(
RestTemplateTransportClientFactories restTemplateTransportClientFactories) {
setTransportClientFactories(restTemplateTransportClientFactories);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
import com.netflix.discovery.util.StringUtil;

/**
* {@link RestTemplate} based implementation of an {@link EurekaHttpClient}.
*
* @author Daniel Lavoie
*/
public class RestTemplateEurekaHttpClient implements EurekaHttpClient {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
*/
public class RestTemplateTransportClientFactories
implements TransportClientFactories<Void> {
private final RestTemplateTransportClientFactory restTemplateTransportClientFactory;

public RestTemplateTransportClientFactories(
RestTemplateTransportClientFactory restTemplateTransportClientFactory) {
this.restTemplateTransportClientFactory = restTemplateTransportClientFactory;
}

@Override
public TransportClientFactory newTransportClientFactory(
Expand All @@ -44,15 +50,15 @@ public TransportClientFactory newTransportClientFactory(
public TransportClientFactory newTransportClientFactory(
EurekaClientConfig clientConfig, Collection<Void> additionalFilters,
InstanceInfo myInstanceInfo) {
return new RestTemplateTransportClientFactory();
return restTemplateTransportClientFactory;
}

@Override
public TransportClientFactory newTransportClientFactory(final EurekaClientConfig clientConfig,
final Collection<Void> additionalFilters,
final InstanceInfo myInstanceInfo,
final Optional<SSLContext> sslContext,
final Optional<HostnameVerifier> hostnameVerifier) {
return new RestTemplateTransportClientFactory();
public TransportClientFactory newTransportClientFactory(
final EurekaClientConfig clientConfig,
final Collection<Void> additionalFilters, final InstanceInfo myInstanceInfo,
final Optional<SSLContext> sslContext,
final Optional<HostnameVerifier> hostnameVerifier) {
return restTemplateTransportClientFactory;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@

package org.springframework.cloud.netflix.eureka.http;

import java.net.URI;
import java.net.URISyntaxException;

import org.springframework.http.client.support.BasicAuthorizationInterceptor;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

Expand Down Expand Up @@ -50,28 +46,22 @@
* @author Daniel Lavoie
*/
public class RestTemplateTransportClientFactory implements TransportClientFactory {
private EurekaRestTemplateFactory eurekaRestTemplateFactory;

@Override
public EurekaHttpClient newClient(EurekaEndpoint serviceUrl) {
return new RestTemplateEurekaHttpClient(restTemplate(serviceUrl.getServiceUrl()),
serviceUrl.getServiceUrl());
public RestTemplateTransportClientFactory(
EurekaRestTemplateFactory eurekaRestTemplateFactory) {
this.eurekaRestTemplateFactory = eurekaRestTemplateFactory;
}

private RestTemplate restTemplate(String serviceUrl) {
RestTemplate restTemplate = new RestTemplate();
try {
URI serviceURI = new URI(serviceUrl);
if (serviceURI.getUserInfo() != null) {
String[] credentials = serviceURI.getUserInfo().split(":");
if (credentials.length == 2) {
restTemplate.getInterceptors().add(new BasicAuthorizationInterceptor(
credentials[0], credentials[1]));
}
}
}
catch (URISyntaxException ignore) {
@Override
public EurekaHttpClient newClient(EurekaEndpoint eurekaEndpoint) {
return new RestTemplateEurekaHttpClient(
newRestTemplate(eurekaEndpoint.getServiceUrl()),
eurekaEndpoint.getServiceUrl());
}

}
private RestTemplate newRestTemplate(String serviceUrl) {
RestTemplate restTemplate = eurekaRestTemplateFactory.newRestTemplate(serviceUrl);

restTemplate.getMessageConverters().add(0, mappingJacksonHttpMessageConverter());

Expand All @@ -96,34 +86,34 @@ public MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter()
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE));

SimpleModule jsonModule = new SimpleModule();
jsonModule.setSerializerModifier(createJsonSerializerModifier());//keyFormatter, compact));
jsonModule.setSerializerModifier(createJsonSerializerModifier());// keyFormatter,
// compact));
converter.getObjectMapper().registerModule(jsonModule);

converter.getObjectMapper().configure(SerializationFeature.WRAP_ROOT_VALUE, true);
converter.getObjectMapper().configure(DeserializationFeature.UNWRAP_ROOT_VALUE,
true);
converter.getObjectMapper().addMixIn(Applications.class, ApplicationsJsonMixIn.class);
converter.getObjectMapper().addMixIn(InstanceInfo.class, InstanceInfoJsonMixIn.class);

// converter.getObjectMapper().addMixIn(DataCenterInfo.class, DataCenterInfoXmlMixIn.class);
// converter.getObjectMapper().addMixIn(InstanceInfo.PortWrapper.class, PortWrapperXmlMixIn.class);
// converter.getObjectMapper().addMixIn(Application.class, ApplicationXmlMixIn.class);
// converter.getObjectMapper().addMixIn(Applications.class, ApplicationsXmlMixIn.class);

converter.getObjectMapper().addMixIn(Applications.class,
ApplicationsJsonMixIn.class);
converter.getObjectMapper().addMixIn(InstanceInfo.class,
InstanceInfoJsonMixIn.class);

return converter;
}

public static BeanSerializerModifier createJsonSerializerModifier() {//final KeyFormatter keyFormatter, final boolean compactMode) {
public static BeanSerializerModifier createJsonSerializerModifier() {
return new BeanSerializerModifier() {
@Override
public JsonSerializer<?> modifySerializer(SerializationConfig config,
BeanDescription beanDesc, JsonSerializer<?> serializer) {
/*if (beanDesc.getBeanClass().isAssignableFrom(Applications.class)) {
return new ApplicationsJsonBeanSerializer((BeanSerializerBase) serializer, keyFormatter);
}*/
BeanDescription beanDesc, JsonSerializer<?> serializer) {
/*
* if (beanDesc.getBeanClass().isAssignableFrom(Applications.class)) {
* return new ApplicationsJsonBeanSerializer((BeanSerializerBase)
* serializer, keyFormatter); }
*/
if (beanDesc.getBeanClass().isAssignableFrom(InstanceInfo.class)) {
return new InstanceInfoJsonBeanSerializer((BeanSerializerBase) serializer, false);
return new InstanceInfoJsonBeanSerializer(
(BeanSerializerBase) serializer, false);
}
return serializer;
}
Expand Down
Loading

0 comments on commit 9ced12a

Please sign in to comment.