本文介绍了如何覆盖Spring Cloud Eureka默认发现客户端默认ssl上下文?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试为spring cloud eureka服务器启用https。
Yaml配置:

I'm trying to enable https for spring cloud eureka server.Yaml config:

server:
  port: 8100
ssl:
  clientAuth: want
  protocol: TLS
  key-store: classpath:keystore/keystore.jks
  key-store-password: some
  key-password: some
eureka:
  instance:
    prefer-ip-address: true
    non-secure-port-enabled: false
    secure-port-enabled: true
    secure-port: ${server.port}
    healthCheckUrl: https://${eureka.hostname}:${secure-port}/health
    statusPageUrl: https://${eureka.hostname}:${secure-port}/info
    homePageUrl: https://${eureka.hostname}:${secure-port}/
security:
  basic:
    enabled: true

然后我启动一个客户端注册到服务器。我没有导入cource的自签名证书我得到了激活 sun.security.provider.certpath.SunCertPathBuilderException:无法找到所请求目标的有效证书路径。我不想导入证书,因为有很多实例和证书管理成本很高。因此,我将cert放在classpath中并在启动时加载它。我通过添加代码来覆盖客户端默认的ssl上下文和ssl socket facotory

Then I start a client to register into the server. I haven't import the self-signed cert thus of cource I get the excpetion sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target. I don't want to import cert because there are plenty instances and certs management cost much. Thus I put the cert in classpath and load it during start up. I override the client default ssl context and ssl socket facotory by adding codes

SSLContext.setDefault(sslContext);
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());

当使用feign调用其他eureka客户端时,它工作正常。但是,在注册到eureka服务器时它没有任何努力。
我检查源代码并找到使用jersey的eureka发现客户端,泽西调用http apache客户端。
麻烦的是,它使用 SchemeRegistryFactory.createDefault(),最终将调用 SSLContexts.createDefault()谁将不会采取系统属性考虑到。换句话说,这个http客户端不会标记我的自定义SSLContexts。
所以我的问题是,有一种方法可以在eureka发现客户端中添加/重新定位/替换默认的http客户端吗?

It works fine when it calls other eureka client using feign. However it has no effort when registering to eureka server.I check the source code and find that eureka discovery client using jersey, jersey call the http apache client.The trouble is, it use SchemeRegistryFactory.createDefault() which will final call SSLContexts.createDefault() who will not taken system properties into consideration. In other words, this http client will not token my custom SSLContexts.So my question is, it there a method to add/resigter/replace the default http client in eureka discovery client?

推荐答案

最后,我在多次挖掘源代码后找到了解决方案。我正在使用版本Camden.SR5,它将调用eureka-client-1.4.12。

Finally I found a solution after digging the source code many times. I'm using version Camden.SR5 which will call eureka-client-1.4.12.

如果你在中提供 EurekaJerseyClient DiscoveryClientOptionalArgs ,然后Discovery客户端将不会初始化默认客户端。类 DiscoveryClient 的部分代码。

If you provide a EurekaJerseyClient in DiscoveryClientOptionalArgs, then the Discovery client will not intial the default one. Part of the code from class DiscoveryClient.

private void scheduleServerEndpointTask(EurekaTransport eurekaTransport,
                                        DiscoveryClientOptionalArgs args) {
...

    EurekaJerseyClient providedJerseyClient = args == null
            ? null
            : args.eurekaJerseyClient;

    eurekaTransport.transportClientFactory = providedJerseyClient == null
            ? TransportClientFactories.newTransportClientFactory(clientConfig, additionalFilters, applicationInfoManager.getInfo())
            : TransportClientFactories.newTransportClientFactory(additionalFilters, providedJerseyClient);
...
}

然后我添加一个类来制作一个 DiscoveryClientOptionalArgs bean。

Then I add a class to make a DiscoveryClientOptionalArgs bean.

import com.netflix.discovery.DiscoveryClient;
import com.netflix.discovery.EurekaClientConfig;
import com.netflix.discovery.converters.wrappers.CodecWrappers;
import com.netflix.discovery.shared.transport.jersey.EurekaJerseyClient;
import com.qy.insurance.cloud.core.eureka.CustomEurekaJerseyClientBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Slf4j
public class EurekaSslConfig {

@Value("${eureka.client.service-url.defaultZone}")
private String defaultZone;

@Autowired
private EurekaClientConfig config;

@Autowired
private DefaultSslConfig defaultSslConfig;

@Bean
public DiscoveryClient.DiscoveryClientOptionalArgs discoveryClientOptionalArgs(){
    if(!defaultSslConfig.isFinish()){
        log.warn("Default SSLContext might not have been updated! Please check!");
    }

    DiscoveryClient.DiscoveryClientOptionalArgs args = new DiscoveryClient.DiscoveryClientOptionalArgs();

    CustomEurekaJerseyClientBuilder clientBuilder = new CustomEurekaJerseyClientBuilder()
            .withClientName("DiscoveryClient-HTTPClient-Custom")
            .withUserAgent("Java-EurekaClient")
            .withConnectionTimeout(config.getEurekaServerConnectTimeoutSeconds() * 1000)
            .withReadTimeout(config.getEurekaServerReadTimeoutSeconds() * 1000)
            .withMaxConnectionsPerHost(config.getEurekaServerTotalConnectionsPerHost())
            .withMaxTotalConnections(config.getEurekaServerTotalConnections())
            .withConnectionIdleTimeout(config.getEurekaConnectionIdleTimeoutSeconds() * 1000)
            .withEncoderWrapper(CodecWrappers.getEncoder(config.getEncoderName()))
            .withDecoderWrapper(CodecWrappers.resolveDecoder(config.getDecoderName(), config.getClientDataAccept()));
    if (defaultZone.startsWith("https://")) {
        clientBuilder.withSystemSSLConfiguration();
    }

    EurekaJerseyClient jerseyClient = clientBuilder.build();
    args.setEurekaJerseyClient(jerseyClient);//Provide custom EurekaJerseyClient to override default one
    return args;
}

}

为确保我的自定义 EurekaJerseyClient 运行良好,我从 EurekaJerseyClientImpl 分叉代码并进行了一些修改。

To insure my custom EurekaJerseyClient working well, I forked the code from EurekaJerseyClientImpl and made some modification.

import com.netflix.discovery.converters.wrappers.CodecWrappers;
import com.netflix.discovery.converters.wrappers.DecoderWrapper;
import com.netflix.discovery.converters.wrappers.EncoderWrapper;
import com.netflix.discovery.provider.DiscoveryJerseyProvider;
import com.netflix.discovery.shared.MonitoredConnectionManager;
import com.netflix.discovery.shared.transport.jersey.EurekaJerseyClient;
import com.netflix.discovery.shared.transport.jersey.EurekaJerseyClientImpl;
import com.netflix.discovery.shared.transport.jersey.SSLSocketFactoryAdapter;
import com.netflix.discovery.util.DiscoveryBuildInfo;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.client.apache4.config.ApacheHttpClient4Config;
import com.sun.jersey.client.apache4.config.DefaultApacheHttpClient4Config;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.impl.conn.SchemeRegistryFactory;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.util.TextUtils;

public class CustomEurekaJerseyClientBuilder {
private boolean systemSSL;
private String clientName;
private int maxConnectionsPerHost;
private int maxTotalConnections;
private String trustStoreFileName;
private String trustStorePassword;
private String userAgent;
private String proxyUserName;
private String proxyPassword;
private String proxyHost;
private String proxyPort;
private int connectionTimeout;
private int readTimeout;
private int connectionIdleTimeout;
private EncoderWrapper encoderWrapper;
private DecoderWrapper decoderWrapper;

public CustomEurekaJerseyClientBuilder withClientName(String clientName) {
    this.clientName = clientName;
    return this;
}

public CustomEurekaJerseyClientBuilder withUserAgent(String userAgent) {
    this.userAgent = userAgent;
    return this;
}

public CustomEurekaJerseyClientBuilder withConnectionTimeout(int connectionTimeout) {
    this.connectionTimeout = connectionTimeout;
    return this;
}

public CustomEurekaJerseyClientBuilder withReadTimeout(int readTimeout) {
    this.readTimeout = readTimeout;
    return this;
}

public CustomEurekaJerseyClientBuilder withConnectionIdleTimeout(int connectionIdleTimeout) {
    this.connectionIdleTimeout = connectionIdleTimeout;
    return this;
}

public CustomEurekaJerseyClientBuilder withMaxConnectionsPerHost(int maxConnectionsPerHost) {
    this.maxConnectionsPerHost = maxConnectionsPerHost;
    return this;
}

public CustomEurekaJerseyClientBuilder withMaxTotalConnections(int maxTotalConnections) {
    this.maxTotalConnections = maxTotalConnections;
    return this;
}

public CustomEurekaJerseyClientBuilder withProxy(String proxyHost, String proxyPort, String user, String password) {
    this.proxyHost = proxyHost;
    this.proxyPort = proxyPort;
    this.proxyUserName = user;
    this.proxyPassword = password;
    return this;
}

public CustomEurekaJerseyClientBuilder withSystemSSLConfiguration() {
    this.systemSSL = true;
    return this;
}

public CustomEurekaJerseyClientBuilder withTrustStoreFile(String trustStoreFileName, String trustStorePassword) {
    this.trustStoreFileName = trustStoreFileName;
    this.trustStorePassword = trustStorePassword;
    return this;
}

public CustomEurekaJerseyClientBuilder withEncoder(String encoderName) {
    return this.withEncoderWrapper(CodecWrappers.getEncoder(encoderName));
}

public CustomEurekaJerseyClientBuilder withEncoderWrapper(EncoderWrapper encoderWrapper) {
    this.encoderWrapper = encoderWrapper;
    return this;
}

public CustomEurekaJerseyClientBuilder withDecoder(String decoderName, String clientDataAccept) {
    return this.withDecoderWrapper(CodecWrappers.resolveDecoder(decoderName, clientDataAccept));
}

public CustomEurekaJerseyClientBuilder withDecoderWrapper(DecoderWrapper decoderWrapper) {
    this.decoderWrapper = decoderWrapper;
    return this;
}

public EurekaJerseyClient build() {
    MyDefaultApacheHttpClient4Config config = new MyDefaultApacheHttpClient4Config();
    try {
        return new EurekaJerseyClientImpl(connectionTimeout, readTimeout, connectionIdleTimeout, config);
    } catch (Throwable e) {
        throw new RuntimeException("Cannot create Jersey client ", e);
    }
}

class MyDefaultApacheHttpClient4Config extends DefaultApacheHttpClient4Config {

    private static final String PROTOCOL = "https";
    private static final String PROTOCOL_SCHEME = "SSL";
    private static final int HTTPS_PORT = 443;
    private static final String KEYSTORE_TYPE = "JKS";

    MyDefaultApacheHttpClient4Config() {
        MonitoredConnectionManager cm;

        if (systemSSL) {
            cm = createSystemSslCM();
        } else {
            cm = createDefaultSslCM();
        }

        if (proxyHost != null) {
            addProxyConfiguration(cm);
        }

        DiscoveryJerseyProvider discoveryJerseyProvider = new DiscoveryJerseyProvider(encoderWrapper, decoderWrapper);
        getSingletons().add(discoveryJerseyProvider);

        // Common properties to all clients
        cm.setDefaultMaxPerRoute(maxConnectionsPerHost);
        cm.setMaxTotal(maxTotalConnections);
        getProperties().put(ApacheHttpClient4Config.PROPERTY_CONNECTION_MANAGER, cm);

        String fullUserAgentName = (userAgent == null ? clientName : userAgent) + "/v" + DiscoveryBuildInfo.buildVersion();
        getProperties().put(CoreProtocolPNames.USER_AGENT, fullUserAgentName);

        // To pin a client to specific server in case redirect happens, we handle redirects directly
        // (see DiscoveryClient.makeRemoteCall methods).
        getProperties().put(ClientConfig.PROPERTY_FOLLOW_REDIRECTS, Boolean.FALSE);
        getProperties().put(ClientPNames.HANDLE_REDIRECTS, Boolean.FALSE);
    }

    private void addProxyConfiguration(MonitoredConnectionManager cm) {
        if (proxyUserName != null && proxyPassword != null) {
            getProperties().put(ApacheHttpClient4Config.PROPERTY_PROXY_USERNAME, proxyUserName);
            getProperties().put(ApacheHttpClient4Config.PROPERTY_PROXY_PASSWORD, proxyPassword);
        } else {
            // Due to bug in apache client, user name/password must always be set.
            // Otherwise proxy configuration is ignored.
            getProperties().put(ApacheHttpClient4Config.PROPERTY_PROXY_USERNAME, "guest");
            getProperties().put(ApacheHttpClient4Config.PROPERTY_PROXY_PASSWORD, "guest");
        }
        getProperties().put(DefaultApacheHttpClient4Config.PROPERTY_PROXY_URI, "http://" + proxyHost + ":" + proxyPort);
    }

    private MonitoredConnectionManager createSystemSslCM() {
        MonitoredConnectionManager cm;
        X509HostnameVerifier hostnameVerifier = SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER;
        SSLConnectionSocketFactory systemSocketFactory = new SSLConnectionSocketFactory(
                (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault(),
                split(System.getProperty("https.protocols")),
                split(System.getProperty("https.cipherSuites")),
                hostnameVerifier);
        SSLSocketFactory sslSocketFactory = new SSLSocketFactoryAdapter(systemSocketFactory);
        SchemeRegistry sslSchemeRegistry = new SchemeRegistry();
        sslSchemeRegistry.register(new Scheme(PROTOCOL, HTTPS_PORT, sslSocketFactory));
        cm = new MonitoredConnectionManager(clientName, sslSchemeRegistry);
        return cm;
    }

    /**
     * @see SchemeRegistryFactory#createDefault()
     */
    private MonitoredConnectionManager createDefaultSslCM() {
        final SchemeRegistry registry = new SchemeRegistry();
        registry.register(
                new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
        registry.register(
                new Scheme("https", 443, new SSLSocketFactoryAdapter(SSLConnectionSocketFactory.getSocketFactory())));
        return new MonitoredConnectionManager(clientName, registry);
    }

    private String[] split(final String s) {
        if (TextUtils.isBlank(s)) {
            return null;
        }
        return s.split(" *, *");
    }
}
}

希望这可以帮助那些谁像我一样在Production JVM中导入证书并不容易。

Hope this could helpe those who is not easy to import cert in Production JVM just like me.

这篇关于如何覆盖Spring Cloud Eureka默认发现客户端默认ssl上下文?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

06-26 12:27