是否可以为 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 实现进行了测试:
jersey-client
(版本 2.23.1)
resteasy-client
(版本 3.0.18.Final)
假设我们需要信任一个自签名的 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 benull
.Returns:
an updated client builder instance.
Throws:
NullPointerException
- in case thesslContext
parameter isnull
.
假设您已将证书添加到 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 referencingHttpClient
, but for some applications it may be necessary to drill down into theHttpClient
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 实现进行了测试:
jersey-client
(版本 2.23.1)resteasy-client
(版本 3.0.18.Final)