【问题标题】:Apache camel SSL connection to restful serviceApache骆驼SSL连接到restful服务
【发布时间】:2018-02-02 11:43:14
【问题描述】:

我正忙于一个项目,我必须使用特定证书对公开的休息服务进行 GET。我正在使用带有 https4 组件的 apache camel 框架。我创建了一个密钥库并使用soapUI对其进行了测试,它连接成功,但是我无法通过我的项目进行连接。

我使用以下页面作为参考:http://camel.apache.org/http4.html

我通过以下配置为 HTTP Client 设置 SSL:

 <spring:sslContextParameters id="sslContextParameters">
    <spring:keyManagers keyPassword="xxxx">
        <spring:keyStore resource="classpath:certificates/keystore.jks" password="xxxx"/>
    </spring:keyManagers>
</spring:sslContextParameters>


<setHeader headerName="CamelHttpMethod">
      <simple>GET</simple>
</setHeader>

我的端点配置为:

<to uri="https4://endpointUrl:9007/v1/{id}?sslContextParametersRef=sslContextParameters"/>

我收到的堆栈跟踪:

 javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1904)
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:279)
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:273)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1446)
at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:209)
at sun.security.ssl.Handshaker.processLoop(Handshaker.java:901)
at sun.security.ssl.Handshaker.process_record(Handshaker.java:837)
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1023)
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1332)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1359)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1343)
at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:394)
at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:353)
at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:141)
at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:353)
at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:380)
at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236)
at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:184)
at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:88)
at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110)
at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:184)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:55)
at org.apache.camel.component.http4.HttpProducer.executeMethod(HttpProducer.java:301)
at org.apache.camel.component.http4.HttpProducer.process(HttpProducer.java:173)
at org.apache.camel.util.AsyncProcessorConverterHelper$ProcessorToAsyncProcessorBridge.process(AsyncProcessorConverterHelper.java:61)
at org.apache.camel.processor.SendProcessor.process(SendProcessor.java:145)
at org.apache.camel.processor.interceptor.TraceInterceptor.process(TraceInterceptor.java:163)
at org.apache.camel.processor.RedeliveryErrorHandler.process(RedeliveryErrorHandler.java:468)
at org.apache.camel.processor.CamelInternalProcessor.process(CamelInternalProcessor.java:197)
at org.apache.camel.processor.Pipeline.process(Pipeline.java:121)
at org.apache.camel.processor.Pipeline.process(Pipeline.java:83)
at org.apache.camel.processor.CamelInternalProcessor.process(CamelInternalProcessor.java:197)
at org.apache.camel.component.direct.DirectProducer.process(DirectProducer.java:62)
at org.apache.camel.impl.InterceptSendToEndpoint$1.process(InterceptSendToEndpoint.java:164)
at org.apache.camel.processor.SendProcessor.process(SendProcessor.java:145)
at org.apache.camel.processor.interceptor.TraceInterceptor.process(TraceInterceptor.java:163)
at org.apache.camel.processor.RedeliveryErrorHandler.process(RedeliveryErrorHandler.java:468)
at org.apache.camel.processor.CamelInternalProcessor.process(CamelInternalProcessor.java:197)
at org.apache.camel.processor.ChoiceProcessor.process(ChoiceProcessor.java:117)
at org.apache.camel.processor.interceptor.TraceInterceptor.process(TraceInterceptor.java:163)
at org.apache.camel.processor.RedeliveryErrorHandler.process(RedeliveryErrorHandler.java:468)
at org.apache.camel.processor.CamelInternalProcessor.process(CamelInternalProcessor.java:197)
at org.apache.camel.processor.Pipeline.process(Pipeline.java:121)
at org.apache.camel.processor.Pipeline.access$100(Pipeline.java:44)
at org.apache.camel.processor.Pipeline$1.done(Pipeline.java:139)
at org.apache.camel.processor.CamelInternalProcessor$InternalCallback.done(CamelInternalProcessor.java:257)
at org.apache.camel.processor.RedeliveryErrorHandler$1.done(RedeliveryErrorHandler.java:480)
at org.apache.camel.processor.interceptor.TraceInterceptor$1.done(TraceInterceptor.java:180)
at org.apache.camel.processor.SendProcessor$1.done(SendProcessor.java:155)
at org.apache.camel.processor.CamelInternalProcessor$InternalCallback.done(CamelInternalProcessor.java:257)
at org.apache.camel.processor.Pipeline$1.done(Pipeline.java:148)
at org.apache.camel.processor.CamelInternalProcessor$InternalCallback.done(CamelInternalProcessor.java:257)
at org.apache.camel.processor.RedeliveryErrorHandler$1.done(RedeliveryErrorHandler.java:480)
at org.apache.camel.processor.interceptor.TraceInterceptor$1.done(TraceInterceptor.java:180)
at org.apache.camel.processor.SendProcessor$1.done(SendProcessor.java:155)
at org.apache.camel.component.cxf.CxfClientCallback.handleResponse(CxfClientCallback.java:61)
at org.apache.cxf.endpoint.ClientImpl.onMessage(ClientImpl.java:827)
at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleResponseInternal(HTTPConduit.java:1672)
at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream$1.run(HTTPConduit.java:1168)
at org.apache.cxf.workqueue.AutomaticWorkQueueImpl$3.run(AutomaticWorkQueueImpl.java:428)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at org.apache.cxf.workqueue.AutomaticWorkQueueImpl$AWQThreadFactory$1.run(AutomaticWorkQueueImpl.java:353)
at java.lang.Thread.run(Thread.java:745)

任何帮助将不胜感激!

【问题讨论】:

标签: apache rest ssl apache-camel


【解决方案1】:

同样:我关注 documented instructions 并陷入“PKIX 路径构建失败:sun.security.provider.certpath.SunCertPathBuilderException:无法找到请求目标的有效认证路径”。有一个快速修复,但如果您想将配置链接到客户端 HTTP 会话,这将成为一个复杂的设置。

方法一: 文档页面、论坛和this other article 会告诉您设置 JVM 启动选项“-Djavax.net.ssl.trustStore=myKeystore.jks -Djavax.net.ssl.trustStorePassword=mystorepass”确实可以解决问题,前提是远程方' 证书(自签名,或由 CA 签名但随后使用所有完整证书链)在提供的密钥库中都作为受信任的证书获取。事实上,HTTP4 基于 JSSE,这些 java 启动选项确实配置了 JVM 范围的堆栈。 作为替代方案,您还可以在默认 JVM 密钥库 jre\lib\security\cacerts(初始密码:“changeit”)中获取对等方的证书(完整链),因此甚至不需要 JVM 选项。

如果您有几个传出客户端连接和少量对等证书,这是最简单的方法。

方法二: 在我们的上下文中,有超过 100 个远程方,每个方平均需要每 2 年更新一次证书,该方法意味着大约每周在更新的密钥库上重新启动 JVM。我们的高可用网关不再是高可用的。所以我搜索了一种动态/每个连接/程序化的方式。

下面是 CAMEL 处理器的代码的简化摘录,我们使用它作为 REST 或普通 HTTP 客户端远程连接,有或没有 SSL/TLS,有或没有客户端证书(即 2-way SSL /TLS 与 1-way SSL/TLS),以及根据对等方的要求组合 HTTP Basic Auth。

由于各种原因,现在旧的 CAMEL 版本 2.16.3 仍在我们的上下文中使用。我还没有测试过更新的版本。考虑到 Apache CAMEL 层下的库,我怀疑没有任何变化。

我在下面的代码中添加了许多 cmets 详细说明变体 API 以达到相同的效果。因此,您有以下线索可以进一步简化代码或尝试使用较新的 HTTP4 版本的替代方案。照原样,代码与 2.16 一起工作,作为 Spring 应用程序上下文中的 CAMEL 处理器 bean,包含 DSL 中的整个 CAMEL 路由定义。

在我们的上下文中,我们使用 java 代码来配置每个会话完全动态的 SSL/TLS 出站连接。您应该可以轻松地将我们在下面通过 java 动态设置的部分配置冻结到适合您的上下文的 CAMEL XML DSL 中。

Maven 依赖关系:

<properties>
    <camel-version>2.16.3</camel-version>
</properties>
...
<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-core</artifactId>
    <version>${camel-version}</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-http4</artifactId>
    <version>${camel-version}</version>
    <scope>provided</scope>
</dependency>

从我们的 org.apache.camel.Processor 中提取的代码(我已经删除了许多异常处理并简化了下面的代码以便专注于解决方案):

// relevant imports (partial)
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.component.http4.HttpClientConfigurer;
import org.apache.camel.component.http4.HttpComponent;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;

...
@Override
public void process(Exchange exchange) throws Exception {
    // assume here that we have previously fetched all dynamic connexion parameters in set of java Properties. Of course you can use numerous means to inject connection parameters
    Properties params= ... ;

    // Trick! 'targetURL' is the URI of the http server to call. Its not the same as the Camel endpoint URI (see further "httpUrlToken" placeHolder), on which you configure endpoint options
    // Fact is, we prefer to pass just the target URL as parameter and keep full control on building the CAMEL endpoint URI in java
    String targetURL= params.getProperty("targetURL"); // URL to call, e.g. "http://remoteHost.com/some/servlet/path". Will override the placeholder URL set on the endpoint.

    // default  plain HTTP without SSL/TLS:
    String endPointURI = "http4://httpUrlToken?throwExceptionOnFailure=false"; // with option to prevent exceptions from being thrown for failed response codes. It allows us to process all the response codes in a response Processor

    // Oh yes! we have to manage a map of HttpComponent instances, because the CAMEL doc clearly tells that each instance can only support a single configuration 
    // and our true connector is multithreading where each request may go to a different (dynamic) destination with different SSL settings, 
    // so we actually use a Map of HttpComponent instances of size MAX_THREADS and indexed by the thread ID plus ageing and re-use strategies... but this brings us too far.
    // So, for a single thread per client instance, you can just do:
    HttpComponent httpComponent = exchange.getContext().getComponent("http4", HttpComponent.class);

    // overload in case of SSL/TLS
    if (targetURL.startsWith("https")) { 
        try {
            endPointURI = "https4://httpUrlToken?throwExceptionOnFailure=false"; 
            httpComponent = exchange.getContext().getComponent("https4", HttpComponent.class); // well: "https4" and "http4" are the same, so you may skip this line! (our true HttpComponent map is common to secured and unsecured client connexions)

            // basic SSL context setup as documented elsewhere, should be enough in theory
            SSLContext sslctxt =  getSSLContext(exchange, params.getProperty("keystoreFilePath"), params.getProperty("keystorePassword"), params.getProperty("authenticationMode")); // cfr helper method below
            HttpClientConfigurer httpClientConfig = getEndpointClientConfigurer(sslctxt); // cfr helper method below
            httpComponent.setHttpClientConfigurer(httpClientConfig);
            // from here, if you skip the rest of the configuration, you'll get the exception "sun.security.provider.certpath.SunCertPathBuilderException:unable to find valid certification path to requested target"
            // the SSL context covers certificate validation but not the host name verification process 
            // we de-activate here at the connection factory level (systematically... you may not want that), and link the later to the HTTP component
            HostnameVerifier hnv = new AllowAll();
            SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslctxt, hnv);
            // You may choose to enforce the BasicHttpClientConnectionManager or PoolingHttpClientConnectionManager, cfr CAMEL docs
            // In addition, the following linkage of the connection factory through a Registry that captures the 'https' scheme to your factory is required
            Registry<ConnectionSocketFactory> lookup = RegistryBuilder.<ConnectionSocketFactory>create().register("https", sslSocketFactory).build();
            HttpClientConnectionManager connManager = new BasicHttpClientConnectionManager(lookup);

            // Does not work in 2.16, as documented at http://camel.apache.org/http4.html#HTTP4-UsingtheJSSEConfigurationUtility
            //              ... keystore and key manager setup ...
            //              SSLContextParameters scp = new SSLContextParameters();
            //              scp.setKeyManagers(...);
            //              httpComponent.setSslContextParameters(scp);

            // Not as good as using a connection manager on the HTTP component, although same effects in theory
            //      HttpClientBuilder clientBuilder = HttpClientBuilder.create();
            //      clientBuilder.set... various parameters...
            //      httpClientConfig.configureHttpClient(clientBuilder);

            // Commented-out alternative method to set BasicAuth with user and password
            //  HttpConfiguration httpConfiguration = new HttpConfiguration();
            //  httpConfiguration.setAuthUsername(authUsername);
            //  ... more settings ...
            //  httpComponent.setHttpConfiguration(httpConfiguration);

            // setClientConnectionManager() is compulsory to prevent "SunCertPathBuilderException: unable to find valid certification path to requested target"
            // if instead we bind the connection manager to a clientBuilder, that doesn't work... 
            httpComponent.setClientConnectionManager(connManager);

        } catch (Exception e) { ... ; }
    } 
    // (back to code common to secured and unsecured client sessions)
    // additional parameters on the endpoint as needed, cfr API docs
    httpComponent.set...(...) ;
    // you may want to append these 3 URI options in case of HTTP[S] with Basic Auth 
    if (... basic Auth needed ...) 
        endPointURI += "&authUsername="+params.getProperty("user")+"&authPassword="+params.getProperty("password")+"&authenticationPreemptive=true";

    // *********** ACTUAL TRANSMISSION ********************
    exchange.getIn().setHeader(Exchange.HTTP_URI, targetURL); // needed to overload the "httpUrlToken" placeholder in the endPointURI
    // Next, there are many ways to get a CAMEL Producer or ProducerTemplate
    // e.g. httpComponent.createEndpoint(endPointURI).createProducer()
    // ... in our case we use a template injected from a Spring application context (i.e. <camel:template id="producerTemplate"/>) via constructor arguments on our Processor bean
    try {
       producerTemplate.send(httpComponent.createEndpoint(endPointURI),exchange);
    } catch (Exception e) { ...; }
    // you can then process the HTTP response here, or better dedicate the next 
    // Processor on the CAMEL route to such handlings...
    ...
}

支持辅助方法,由上述代码调用

private HttpClientConfigurer getEndpointClientConfigurer(final SSLContext sslContext) {
    return new HttpClientConfigurer(){
        @Override
        public void configureHttpClient(HttpClientBuilder clientBuilder) {
          // I put a logger trace here to see if/when the ssl context is actually applied, the outcome was ... weird, try it!
          clientBuilder.setSSLContext(sslContext);
        }
    };
}

/**
 * Build a SSL context with keystore and other parameters according to authentication mode.
 * The keystore may just contain a trusted peer's certificate for 1way cases, and the associated certificate chain up to a trusted root as applicable.
 * The keystore shall too contain one single client private key and certificate for 2way modes. We assume here a same password on keystore and private key.
 * @param authenticationMode one of "1waySSL" "1wayTLS" "2waySSL" "2wayTLS" each possibly suffixed by "noCHECK" as in "1waySSLnoCHECK"
 * @param keystoreFilePath can be null for "noCHECK" modes
 * @param keystorePassword would be null if above is null
 */
private SSLContext getSSLContext(Exchange exchange, String keystoreFilePath, String keystorePassword, String authenticationMode) throws GeneralSecurityException, FileNotFoundException, IOException {
    SSLContext sslContext = SSLContext.getInstance(authenticationMode.substring(4,7).toUpperCase(),"SunJSSE"); 

    //enforce Trust ALL ? pass a trust manager that does not validate certificate chains
    if (authenticationMode.endsWith("noCHECK")) {
        TrustManager[] trustAllCerts = new TrustManager[]{ new TrustALLManager()};
        sslContext.init(null , trustAllCerts, null);
        return sslContext; 
    }

    // we use https, and validate remote cert's by default, henceforth keystore and password become compulsory
    if (null == keystoreFilePath || null == keystorePassword)
        throw new GeneralSecurityException("Config ERROR: using https://... and implicit default AUTHMODE=1waySSL altogether requires to supply keystore parameters");
    KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
    TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
    trustStore.load(new FileInputStream(keystoreFilePath), keystorePassword.toCharArray());
    tmf.init(trustStore);
    KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    if (authenticationMode.charAt(0)=='2') { // our authenticationMode starts with 1way.. or 2way...
        // 2way... case: set the keystore parameters accordingly
        keyStore.load(new FileInputStream(keystoreFilePath), keystorePassword.toCharArray());
        kmf.init(keyStore, keystorePassword.toCharArray());
        sslContext.init(kmf.getKeyManagers() , tmf.getTrustManagers(), new SecureRandom());
    } else {  // 1way... case
        sslContext.init(null , tmf.getTrustManagers(), new SecureRandom());
    }
    return sslContext;
}

// Create a trust manager that does not validate certificate chains
private class TrustALLManager implements X509TrustManager {
    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { }
    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { }
    @Override
    public X509Certificate[] getAcceptedIssuers() {
      return new X509Certificate[0];
    }
}

private static class AllowAll implements HostnameVerifier
{
    @Override
    public boolean verify(String arg0, SSLSession arg1) {
      return true;
    }
}

}

希望这会有所帮助。我花了很多时间试图让它工作(尽管我很了解 SSL/TLS 原则、安全性、X509 等)......这段代码与我对干净和精简的 Java 代码的品味相去甚远。此外,我假设您确实知道如何构建密钥库、提供所有需要的证书链、定义 CAMEL 路由等。因此,它可以在 Spring 应用程序上下文中与 Camel 2.16 一起使用,并且除了提供线索之外别无他法会节省你的时间。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-07
    相关资源
    最近更新 更多