如何覆盖Spring Cloud Eureka默认发现客户端默认ssl上下文?

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

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的自签名证书我得到了exc.security.provider.certpath.SunCertPathBuilderException:无法找到所请求目标的有效证书路径.我不想导入证书,因为有很多实例和证书管理成本很高.因此,我将cert放在classpath中并在启动时加载它.我通过添加代码覆盖客户端默认的ssl上下文和ssl socket facotory

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

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

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

如果您在DiscoveryClientOptionalArgs中提供EurekaJerseyClient,则Discovery客户端将不会初始化默认客户端. 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.

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分叉代码并进行了一些修改.

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(" *, *");
    }
}
}

希望这可以帮助那些不像我一样在生产JVM中导入证书的人.

转载注明原文:如何覆盖Spring Cloud Eureka默认发现客户端默认ssl上下文? - 代码日志