是否可以为 javax.ws.rs(Resteasy 实现)重用 Java 可信证书?

Is it possible to reuse Java trusted certificates for javax.ws.rs (Resteasy implementation)?

假设我们需要信任一个自签名的 SSL 证书。例如,让我们使用 https://self-signed.badssl.com/.

由于签名者不是 "proper" 授权机构,Java 不信任它并拒绝连接到该服务器。然而,

之后
$ cd $JAVA_HOME/jre/lib/security
$ keytool -import -trustcacerts -alias ... -file ... -keystore cacerts

并重新启动应用程序,以下代码有效:

new URL ("https://self-signed.badssl.com/").openConnection ().getResponseCode ()

和 returns 200(OK),没有抛出异常。 IE。打开 HTTPS 连接的基本 Java 方式现在可用,因为证书现在是可信的。

然而,这对 javax.ws.rs 客户端没有任何可见的影响(至少在 Resteasy 中实现),我仍然遇到异常:

javax.ws.rs.ProcessingException: Unable to invoke request
        at org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine.invoke(ApacheHttpClient4Engine.java:287)
        at org.jboss.resteasy.client.jaxrs.internal.ClientInvocation.invoke(ClientInvocation.java:407)
        at org.jboss.resteasy.client.jaxrs.internal.ClientInvocationBuilder.method(ClientInvocationBuilder.java:273)
        [...]
Caused by: 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:1949)
        at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302)
        at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:296)
        at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1506)
        at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:216)
        at sun.security.ssl.Handshaker.processLoop(Handshaker.java:979)
        at sun.security.ssl.Handshaker.process_record(Handshaker.java:914)
        at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1062)
        at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375)
        at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403)
        at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387)
        at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:535)
        at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:403)
        at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:177)
        at org.apache.http.impl.conn.ManagedClientConnectionImpl.open(ManagedClientConnectionImpl.java:304)
        at org.apache.http.impl.client.DefaultRequestDirector.tryConnect(DefaultRequestDirector.java:611)
        at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:446)
        at org.apache.http.impl.client.AbstractHttpClient.doExecute(AbstractHttpClient.java:863)
        at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
        at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:57)
        at org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine.invoke(ApacheHttpClient4Engine.java:283)
        ... 90 more
Caused by: 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.validator.PKIXValidator.doBuild(PKIXValidator.java:387)
        at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:292)
        at sun.security.validator.Validator.validate(Validator.java:260)
        at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:324)
        at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:229)
        at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:124)
        at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1488)
        ... 107 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
        at sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:146)
        at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:131)
        at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:280)
        at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:382)
        ... 113 more

Resteasy 似乎没有考虑 "standard" 密钥库。但我宁愿有一个中央(特定于机器的)位置来存放额外的可信密钥,而不用担心应用程序如何使用它们,URL.openConnection 或 javax.ws.rs.

问题 是否可以使 javax.ws.rs Client 使用 与 "normal" Java HTTPS 连接机制相同的 密钥库?

创建 Client 实例时设置 SSL 上下文

ClientBuilder API there's a method that allows you to set the SSLContext:

public abstract ClientBuilder sslContext(SSLContext sslContext)

Set the SSL context that will be used when creating secured transport connections to server endpoints from web targets created by the client instance that is using this SSL context. The SSL context is expected to have all the security infrastructure initialized, including the key and trust managers.

Setting a SSL context instance resets any key store or trust store values previously specified.

Parameters:

sslContext - secure socket protocol implementation which acts as a factory for secure socket factories or SSL engines. Must not be null.

Returns:

an updated client builder instance.

Throws:

NullPointerException - in case the sslContext parameter is null.

假设您已将证书添加到 cacerts 信任库,您可以使用默认的 SSLContext when creating your Client 实例。

Client client = ClientBuilder.newBuilder().sslContext(SSLContext.getDefault()).build();

应该就够了。但是,出于某种原因,上面的代码片段不适用于 RESTEasy,但适用于 Jersey。这很可能是 RESTEasy 错误。

标准解决方案不适用于 RESTEasy。我该怎么办?

RESTEasy documentation 声明如下:

Network communication between the client and server is handled in RESTEasy, by default, by HttpClient (4.x) from the Apache HttpComponents project. [...]

RESTEasy and HttpClient make reasonable default decisions so that it is possible to use the client framework without ever referencing HttpClient, but for some applications it may be necessary to drill down into the HttpClient details. [...]

要自定义 RESTEeasy 使用的 HttpClient,请执行以下操作:

HttpClient httpClient = HttpClientBuilder.create()
                                         .setSslcontext(SSLContext.getDefault())
                                         .build();

ApacheHttpClient4Engine engine = new ApacheHttpClient4Engine(httpClient);
Client client = new ResteasyClientBuilder().httpEngine(engine).build();

然后就可以执行请求了:

Response response = client.target("https://self-signed.badssl.com/").request().get();
System.out.println(response.getStatus());

是否有 SSL 上下文的替代方案?

您可以加​​载 KeyStore 而不是使用 SSLContext when creating your Client。要加载 cacerts 信任库,您可以执行以下操作:

String filename = System.getProperty("java.home") + 
        "/lib/security/cacerts".replace('/', File.separatorChar);
FileInputStream is = new FileInputStream(filename);
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
String password = "changeit";
keystore.load(is, password.toCharArray());

cacerts的默认密码是changeit

然后使用以下方法之一创建您的 Client 实例:

Client client = ClientBuilder.newBuilder().trustStore(keystore).build();
Client client = ClientBuilder.newBuilder().keyStore(keystore, password).build();

问题是它不适用于 RESTEasy,但适用于 Jersey。


上述解决方案针对以下 JAX-RS 客户端 API 实现进行了测试: