FeignClient With Client Certificate And Docker

Hank picture Hank · May 9, 2016 · Viewed 7.8k times · Source

I have a requirement for my microservices to utilize two way ssl. Each microservice is a Spring Boot Application, annotated with:

@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
@EnableZuulProxy
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Each yml has similar configuration for eureka/ribbon:

eureka:
    client:
        service-url:
          defaultZone: ${EUREKA_CLIENT_SERVICEURL_PROTOCOL:http}://${EUREKA_CLIENT_SERVICEURL_HOST:192.168.99.100}:${EUREKA_CLIENT_SERVICEURL_PORT:8761}/eureka/
    instance:
        secure-virtual-host-name: ${spring.application.name}
        prefer-ip-address: true
        non-secure-port-enabled: ${EUREKA_NON_SECURE_PORT_ENABLED:false}
        secure-port-enabled: ${EUREKA_SECURE_PORT_ENABLED:true}
        secure-port: ${server.port}
ribbon:
    IsSecure: true
    eureka:
        enabled: true

Each microservice has a controller that exposes a rest api for various functionality.

When one microservices needs to call upon another microservice endpoint, I'm trying to do so by creating a client interface to that microservice:

@FeignClient(name = "user", configuration = FeignConfiguration.class, url = "https://user")
public interface UserClient {
    @RequestMapping(method = RequestMethod.GET, value = "/test")
    String testUser();
}

Here's the FeignConfiguration:

@Configuration
public class FeignConfiguration {

    @Bean
    public Feign.Builder feignBuilder(){
        Client trustSSLSockets = new    Client.Default(TrustingSSLSocketFactory.get(), null);
        return Feign.builder().client(trustSSLSockets);
    }

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

    @Bean
    public Contract feignContract() {
        return new feign.Contract.Default();
    }

}

The TrustingSSLSocketFactory class was copied from: https://github.com/Netflix/feign/blob/master/core/src/test/java/feign/client/TrustingSSLSocketFactory.java and modified with my client cert/password.

The User microservice, among other configs, has security configured as:

server:
    port: ${DOCKER_SERVER_PORT:28443}
      ssl:
          key-store: ${KEYSTORE_FILE:classpath:keystore.p12}
          key-store-password: ${KEYSTORE_PASSWORD:abc123}
          key-store-type: ${KEYSTORE_TYPE:PKCS12}
          trust-store: ${TRUSTSTORE_FILE:classpath:trust.jks}
          trust-store-password: ${TRUSTSTORE_PASSWORD:abc123}
          trust-store-type: ${TRUSTSTORE_TYPE:JKS}
          client-auth: ${CLIENT_AUTH_REQUIRED:want}

To call the client, I simply inject the client, and call it as any other interface.

@RestController
public class MyController {

@Autowired
UserClient userClient;

@RequestMapping(value = "/testUser",
        method = RequestMethod.GET,
        produces = MediaType.APPLICATION_JSON_VALUE)
    public String testUser() {
        return userClient.testUser();
    }
}

The user microservice has a test endpoint that returns a simple reply.

I'm getting an UnknownHostException when the UserClient is trying to establish a connection with the user host. I'm probably not implementing this correctly and would appreciate some guidance.

gateway-ms_1   | 2016-05-09 16:22:19.294 DEBUG 1 --- [qtp351251459-24]     c.n.zuul.http.HttpServletRequestWrapper  : Path = null
gateway-ms_1   | 2016-05-09 16:22:19.294 DEBUG 1 --- [qtp351251459-24] c.n.zuul.http.HttpServletRequestWrapper  : Transfer-Encoding = null
gateway-ms_1   | 2016-05-09 16:22:19.294 DEBUG 1 --- [qtp351251459-24] c.n.zuul.http.HttpServletRequestWrapper  : Content-Encoding = null
gateway-ms_1   | 2016-05-09 16:22:19.295 DEBUG 1 --- [qtp351251459-24] c.n.zuul.http.HttpServletRequestWrapper  : Content-Length header = -1
gateway-ms_1   | 2016-05-09 16:22:19.297 DEBUG 1 --- [qtp351251459-24] c.n.loadbalancer.ZoneAwareLoadBalancer   : Zone aware logic disabled or there is only one zone
gateway-ms_1   | 2016-05-09 16:22:19.298 DEBUG 1 --- [qtp351251459-24] c.n.loadbalancer.LoadBalancerContext     : register using LB returned Server: 172.17.0.4:28443 for request /testUser
gateway-ms_1   | 2016-05-09 16:22:19.298 DEBUG 1 --- [qtp351251459-24] com.netflix.niws.client.http.RestClient  : RestClient sending new Request(GET: ) https://172.17.0.4:28443/testUser
gateway-ms_1   | 2016-05-09 16:22:19.300 DEBUG 1 --- [qtp351251459-24] c.n.http4.MonitoredConnectionManager     : Get connection: {s}->https://172.17.0.4:28443, timeout = 3000
gateway-ms_1   | 2016-05-09 16:22:19.300 DEBUG 1 --- [qtp351251459-24] com.netflix.http4.NamedConnectionPool    : [{s}->https://172.17.0.4:28443] total kept alive: 1, total issued: 0, total allocated: 1 out of 200
gateway-ms_1   | 2016-05-09 16:22:19.300 DEBUG 1 --- [qtp351251459-24] com.netflix.http4.NamedConnectionPool    : Getting free connection [{s}->https://172.17.0.4:28443][null]
gateway-ms_1   | 2016-05-09 16:22:19.300 DEBUG 1 --- [qtp351251459-24] com.netflix.http4.NFHttpClient           : Stale connection check
gateway-ms_1   | 2016-05-09 16:22:19.305 DEBUG 1 --- [qtp351251459-24] com.netflix.http4.NFHttpClient           : Attempt 1 to execute request
register-ms_1    | 2016-05-09 16:22:19.312 DEBUG 1 --- [qtp376795121-24] com.jdh.register.clients.UserClient       : [UserClient#testUser] ---> GET https://user/test HTTP/1.1
register-ms_1    | 2016-05-09 16:22:19.313 DEBUG 1 --- [qtp376795121-24] com.jdh.register.clients.UserClient       : [UserClient#testUser] ---> END HTTP (0-byte body)
register-ms_1    | 2016-05-09 16:22:19.422 DEBUG 1 --- [qtp376795121-24] com.jdh.register.clients.UserClient       : [UserClient#testUser] <--- ERROR UnknownHostException: user (109ms)
register-ms_1    | 2016-05-09 16:22:19.426 DEBUG 1 --- [qtp376795121-24] com.jdh.register.clients.UserClient       : [UserClient#testUser] java.net.UnknownHostException: user
register-ms_1    |  at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:184)
register-ms_1    |  at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
register-ms_1    |  at java.net.Socket.connect(Socket.java:589)
register-ms_1    |  at sun.net.NetworkClient.doConnect(NetworkClient.java:175)
register-ms_1    |  at sun.net.www.http.HttpClient.openServer(HttpClient.java:432)
register-ms_1    |  at sun.net.www.http.HttpClient.openServer(HttpClient.java:527)
register-ms_1    |  at sun.net.www.protocol.https.HttpsClient.<init>(HttpsClient.java:275)
register-ms_1    |  at sun.net.www.protocol.https.HttpsClient.New(HttpsClient.java:371)
register-ms_1    |  at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.getNewHttpClient(AbstractDelegateHttpsURLConnection.java:191)
register-ms_1    |  at sun.net.www.protocol.http.HttpURLConnection.plainConnect0(HttpURLConnection.java:1104)
register-ms_1    |  at sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConnection.java:998)
register-ms_1    |  at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:177)
register-ms_1    |  at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1512)
evice-ms_1    |     at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1440)

Answer

spencergibb picture spencergibb · May 10, 2016

The feign Client is where ribbon is injected. By creating your own client, you have opted out of ribbon, hence http://user/... isn't resolved by ribbon.

This is how we create the load balancer client

@Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
        SpringClientFactory clientFactory) {
    return new LoadBalancerFeignClient(new Client.Default(null, null),
            cachingFactory, clientFactory);
}

Also, by using feign.Contract.Default() you opt out of using Spring MVC annotations, like @RequestMapping and default to feign's annotations.