您的位置:首页 > 理论基础 > 计算机网络

HTTPClient 4.5.1 访问https

2016-07-06 00:00 316 查看
摘要: 由于对方提供的接口是基于https 协议,并且是自己生成的证书,处理中遇到一些问题,这里记录一下。

注:除了该博客提供的两种方式以外,其实还有其他的解决方案,原理都是忽略或者所有认证都设置为受信,在处理这个问题走了不少弯路。

EXCEPTION 1 :Host name does not match the certificate subject provided by the peer

//关闭主机名的验证
HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;

EXCEPTION 2:The server failed to respond with a valid HTTP response

防火墙的原因,妈蛋公司的防火墙已经不止一次挑战我的极限了,今天要是早想到这点现在就早闪人了,擦!不管怎么说还是感谢原博主,这里仅作记录。

Apache HttpComponents/Clientで、SSL証明書の検証、ホスト名の検証を無効化する

Java

開発中のテスト環境とかで、よくあるネタ?的な。

Apache HttpComponents/Clientを使って、SSL自己署名証明書を使って通信したり、ホスト名の検証を無効化する方法について、メモとして書いておきます。

Apache HttpComponents - Apache HttpComponents

いつも微妙にやり方を忘れて、毎度毎度調べることになっているので、備忘録的にと。

もちろん、ご利用はテストなどでの範囲で、ですね。

ちなみに、java.net.URLConnectionを使う場合については、以前書きました。

JavaでSSL証明書の検証無効化、ホスト名検証の無効化…とデバッグ - CLOVER

準備

Apache HttpComponentsって、バージョンでけっこうコロコロAPIが変わるので、「これ!」というのは言いづらいのですが、今回はApache HttpComponents(というかClient)の4.5.1を対象にします。

<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.1</version>
</dependency>


テストコードには、JUnit+AssertJを利用。

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</dependency>


また、テスト用のSSLを有効にしたWebサーバーは、Ubuntu LinuxでインストールできるSSLを有効にしたApacheとしました。

サンプルコードで使うimport文

以降のサンプルコードでは、以下のimport文があることを前提にしています。

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException;

import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;


ではでは、書いていってみます。

事象その1、自己署名証明書なのでエラーになる

通信時に、こんなスタックトレースが出力されるようなケース。

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:1509)
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)


SSL証明書の検証で、NGとなる場合ですね。自己署名証明書だと、これに遭遇すると思います。

テストコードは、こんな感じ。

@Test
public void testSelfSignedFailure() throws IOException {
try (CloseableHttpClient client = HttpClients.createDefault()) {
HttpGet get = new HttpGet("https://localhost");

assertThatThrownBy(() -> {
try (CloseableHttpResponse response = client.execute(get)) {
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_OK);
assertThat(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8))
.contains("apache2");
}
})
.isInstanceOf(SSLHandshakeException.class)
.hasMessageContaining("sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target");
}
}


自己署名証明書でもOKにするには

これをパスするには、以下のようなコードを書いてSSLContextを作成します。

TrustStrategy trustStrategy = new TrustSelfSignedStrategy();

SSLContext sslContext =
SSLContexts
.custom()
.loadTrustMaterial(trustStrategy)
.build();

TrustStrategyの実装である、TrustSelfSignedStrategyを使用することで自己署名証明書でもOKになりますよ、と。

で、こちらを使ってHttpClientを作成する、と。

try (CloseableHttpClient client =
HttpClients
.custom()
.setSSLContext(sslContext)
.build()) {
HttpGet get = new HttpGet("https://localhost");


これで、証明書のエラーは回避できます。

事象その2、ホスト名の検証でエラーになる

これだけではまだエラーになる場合が、SSL証明書に書かれているホスト名が、実際にアクセスしているホストと合わない場合。

このケースだと、このようなスタックトレースが得られます。

javax.net.ssl.SSLPeerUnverifiedException: Host name 'localhost' does not match the certificate subject provided by the peer (CN=e611e15f9c9d)
at org.apache.http.conn.ssl.SSLConnectionSocketFactory.verifyHostname(SSLConnectionSocketFactory.java:465)
at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:395)
at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:353)
at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:134)
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:107)


先ほどのSSL証明書の検証を行わないだけのコードは、このような感じになっています。


@Test
public void testHostNameFailure() throws IOException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {TrustStrategy trustStrategy = new TrustSelfSignedStrategy(); SSLContext sslContext = SSLContexts .custom() .loadTrustMaterial(trustStrategy) .build();
assertThatThrownBy(() -> {try (CloseableHttpClient client = HttpClients .custom() .setSSLContext(sslContext) .build()) { HttpGet get = new HttpGet("https://localhost");

try (CloseableHttpResponse response = client.execute(get)) {
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_OK);
assertThat(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8))
.contains("apache2");
}
}
})
.isInstanceOf(SSLPeerUnverifiedException.class)
.hasMessageContaining("Host name 'localhost' does not match the certificate subject provided by the peer (CN=e611e15f9c9d)");
}


が、これだと今回用意した環境では、ホスト名が証明書とアクセスパスで不一致のため、エラーになると。

そこで、ここではHostnameVerifierを使用します。

HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;

NoopHostnameVerifierを使用すると、ホスト名の検証を無効化できます。

SSL証明書の検証と、ホスト名の検証を全部合わせたコードは、このような形になります。


@Test
public void testSelfSignedSuccess() throws IOException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {TrustStrategy trustStrategy = new TrustSelfSignedStrategy(); SSLContext sslContext = SSLContexts .custom() .loadTrustMaterial(trustStrategy) .build();
HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;try (CloseableHttpClient client = HttpClients .custom() .setSSLContext(sslContext) .setSSLHostnameVerifier(hostnameVerifier) .build()) {
HttpGet get = new HttpGet("https://localhost");

try (CloseableHttpResponse response = client.execute(get)) {
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_OK);
assertThat(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8))
.contains("apache2");
}
}
}


HostnameVerifierは、setSSLHostnameVerifierで指定します。

try (CloseableHttpClient client =
HttpClients
.custom()
.setSSLContext(sslContext)
.setSSLHostnameVerifier(hostnameVerifier)
.build()) {


とりあえず、目的は達成できましたよっと。

別解

SSLContextとSSLConnectionSocketFactoryの組み合わせでも、同様のことが実現できます。


@Test
public void testSelfSignedSuccessAnother() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException, IOException {TrustStrategy trustStrategy = new TrustSelfSignedStrategy(); SSLContext sslContext = SSLContexts .custom() .loadTrustMaterial(trustStrategy) .build();
HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;

SSLConnectionSocketFactory sslConnSocketFactory = new
SSLConnectionSocketFactory(sslContext,
hostnameVerifier);
try (CloseableHttpClient client = HttpClients .custom() .setSSLSocketFactory(sslConnSocketFactory) .build()) {
HttpGet get = new HttpGet("https://localhost");

try (CloseableHttpResponse response = client.execute(get)) {
assertThat(response.getStatusLine().getStatusCode())
.isEqualTo(HttpStatus.SC_OK);
assertThat(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8))
.contains("apache2");
}
}
}


この場合は、setSSLSocketFactoryを使用します。

try (CloseableHttpClient client =
HttpClients
.custom()
.setSSLSocketFactory(sslConnSocketFactory)
.build()) {
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  https ssl httpclient