如何让Apache HttpClient 支持SSL配置?

前置准备

  1. SpringBoot项目
  2. Maven依赖
  3. 依赖类

SpringBoot 项目

网络上有很多教程,这里不多介绍。

Maven

代码语言:javascript
复制
<dependency>  
    <groupId>org.apache.httpcomponents</groupId>  
    <artifactId>httpclient</artifactId>  
    <version>4.5.14</version>  
</dependency>

依赖类

按照Httpclient的设计要求编写一个响应信息处理类,也可以使用匿名内部类或者Lambda表达式。

代码语言:javascript
复制
public class CustomHttpClientResponseHandler implements ResponseHandler<HttpResponse> {
@Override
public HttpResponse handleResponse(HttpResponse httpResponse) throws ClientProtocolException, IOException {
    return httpResponse;
}

}

1、 概述

本文介绍如何配置Apache HttpClient 4和5 支持不需要校验证书的接收SSL请求,后文将会介绍如何编写发送带证书的HTTPS请求的HttpClient工具类,以及个人如何在工作中实践工具类使用。

如果不了解HttpClient使用,可以看这篇文章:

HttpClient 指南[1].

2、 SSLPeerUnverifiedException

如果没有在 HttpClient 中配置 SSL 证书,下面的测试(使用 HTTPS URL)就会失败:

代码语言:javascript
复制
/**

    1. The SSLPeerUnverifiedException * Without configuring SSL with the HttpClient, the following test – consuming an HTTPS URL – will fail: */public class SSLPeerUnverifiedException {

    public static void main(String [] args) throws IOException {
    String urlOverHttps = "https://whitestore.top/";
    HttpGet getMethod = new HttpGet(urlOverHttps);
    CloseableHttpClient httpClient = HttpClients.createDefault();
    HttpResponse response = httpClient.execute(getMethod, new CustomHttpClientResponseHandler());
    System.out.println(Objects.equals(response.getStatusLine().getStatusCode(), 200));
    }
    }

它的错误信息有可能是这样的:

代码语言:javascript
复制
javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
at sun.security.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:397)
at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:126)
...

3、 兼容HttpClient 5版本配置

现在,让我们配置 HTTP 客户端信任所有证书链,无论是否有效:

代码语言:javascript
复制
/**

  • 兼容HttpClient 5版本配置
    */
    public class Http5ClientConfig {

    public static void main(String[] args) throws GeneralSecurityException, IOException {

    final HttpGet getMethod = new HttpGet(&#34;https://whitestore.top/&#34;);  
    
    final TrustStrategy acceptingTrustStrategy = (cert, authType) -&gt; true;  
    final SSLContext sslContext = SSLContexts.custom()  
            .loadTrustMaterial(null, acceptingTrustStrategy)  
            .build();  
    final SSLConnectionSocketFactory sslsf =  
            new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);  
    final Registry&lt;ConnectionSocketFactory&gt; socketFactoryRegistry =  
            RegistryBuilder.&lt;ConnectionSocketFactory&gt; create()  
                    .register(&#34;https&#34;, sslsf)  
                    .register(&#34;http&#34;, new PlainConnectionSocketFactory())  
                    .build();  
    
    final BasicHttpClientConnectionManager connectionManager =  
            new BasicHttpClientConnectionManager(socketFactoryRegistry);  
    
    try( CloseableHttpClient httpClient = HttpClients.custom()  
            .setConnectionManager(connectionManager)  
            .build();  
    
         CloseableHttpResponse response = (CloseableHttpResponse) httpClient  
                 .execute(getMethod, new CustomHttpClientResponseHandler())) {  
    
        final int statusCode = response.getStatusLine().getStatusCode();  
        System.out.println(Objects.equals(response.getStatusLine().getStatusCode(), 200));  
    
    }  
    

    }
    }

  • TrustStrategy 替代了标准的证书验证过程,代码这里直接返回true允许所有的证书,允许客户端能够访问HTTPS URL。

    TrustStrategy 这里是比较经典的策略模式设计。

    4、 兼容HttpClient 4.5版本配置

    代码语言:javascript
    复制
    /**

  • 兼容HttpClient 4.5版本配置
    */
    public class Http45ClientConfig {

    public static void main(String[] args) throws GeneralSecurityException, IOException {

     TrustStrategy acceptingTrustStrategy = (cert, authType) -&gt; true;  
     SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();  
     SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext,  
             NoopHostnameVerifier.INSTANCE);  
    
     Registry&lt;ConnectionSocketFactory&gt; socketFactoryRegistry =  
             RegistryBuilder.&lt;ConnectionSocketFactory&gt; create()  
                     .register(&#34;https&#34;, sslsf)  
                     .register(&#34;http&#34;, new PlainConnectionSocketFactory())  
                     .build();  
    
     BasicHttpClientConnectionManager connectionManager =  
             new BasicHttpClientConnectionManager(socketFactoryRegistry);  
     CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)  
             .setConnectionManager(connectionManager).build();  
    
     HttpComponentsClientHttpRequestFactory requestFactory =  
             new HttpComponentsClientHttpRequestFactory(httpClient);  
     ResponseEntity&lt;String&gt; response = new RestTemplate(requestFactory)  
             .exchange(&#34;https://whitestore.top/&#34;, HttpMethod.GET, null, String.class);  
     System.out.println(Objects.equals(response.getStatusCodeValue(), 200));  
    

    }
    }

  • 5、带 SSL 的 Spring RestTemplate(HttpClient 5)

    上面是HttpClient的单独配置,如果想要在Spring RestTemplate中使用,让我们来看看更高级别的客户端:Spring RestTemplate。

    1. 在未配置 SSL 的情况下,直接调用clientNoHttp即可。
    2. 在配置SSL配置之后,则调用configSSLAndClientHttps,这里通过忽略证书校验的逻辑处理,支持外部的SSL访问。
    代码语言:javascript
    复制

    /**

  • 带 SSL 的 Spring RestTemplate(HttpClient 5)
    */
    public class RestTemplate5 {

    public static void main(String[] args) throws GeneralSecurityException {
    final String urlOverHttps = "https://whitestore.top/";
    clientNoHttp(urlOverHttps);
    configSSLAndClientHttps(urlOverHttps);

    }

    private static void clientNoHttp(String urlOverHttps) {
    final ResponseEntity<String> response = new RestTemplate()
    .exchange(urlOverHttps, HttpMethod.GET, null, String.class);
    System.out.println(Objects.equals(response.getStatusCodeValue(), 200));
    }

    public static void configSSLAndClientHttps(String urlOverHttps) throws GeneralSecurityException {
    final TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
    final SSLContext sslContext = SSLContexts.custom()
    .loadTrustMaterial(null, acceptingTrustStrategy)
    .build();
    final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
    final Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory> create()
    .register("https", sslsf)
    .register("http", new PlainConnectionSocketFactory())
    .build();

     final BasicHttpClientConnectionManager connectionManager =  
             new BasicHttpClientConnectionManager(socketFactoryRegistry);  
     final CloseableHttpClient httpClient = HttpClients.custom()  
             .setConnectionManager(connectionManager)  
             .build();  
    
     final HttpComponentsClientHttpRequestFactory requestFactory =  
             new HttpComponentsClientHttpRequestFactory(httpClient);  
     final ResponseEntity&lt;String&gt; response = new RestTemplate(requestFactory)  
             .exchange(urlOverHttps, HttpMethod.GET, null, String.class);  
     System.out.println(Objects.equals(response.getStatusCodeValue(), 200));  
    

    }
    }

  • Spring RestTemplate和原始 HttpClient 配置 SSL 的方式非常相似,都是配置支持 SSL 的请求工厂,然后通过预配置的工厂实例化模板发送请求。

    6、 带 SSL 的 Spring RestTemplate(HttpClient 4.5)

    HttpClient 5版本类似。

    代码语言:javascript
    复制
    /**

  • 带 SSL 的 Spring RestTemplate(HttpClient 4.5)
    */
    public class RestTemplate45 {

    public static void main(String[] args) {
    final CloseableHttpClient httpClient = HttpClients.custom()
    .setSSLHostnameVerifier(new NoopHostnameVerifier())
    .build();
    final HttpComponentsClientHttpRequestFactory requestFactory
    = new HttpComponentsClientHttpRequestFactory();
    requestFactory.setHttpClient(httpClient);

    final ResponseEntity&lt;String&gt; response = new RestTemplate(requestFactory)
            .exchange(&#34;https://whitestore.top/&#34;, HttpMethod.GET, null, String.class);
    System.out.println(Objects.equals(response.getStatusCodeValue(), 200));
    

    }
    }

  • 7. HttpClient 请求工具类:支持发送带证书的HTTPS

    上面介绍的都是服务端如何跳过证书校验的HTTPS请求。

    下面介绍如何发送带证书的HTTPS请求。

    代码语言:javascript
    复制

    import cn.hutool.core.exceptions.ExceptionUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.io.IOUtils;
    import org.apache.http.HttpEntity;
    import org.apache.http.client.ClientProtocolException;
    import org.apache.http.client.methods.CloseableHttpResponse;
    import org.apache.http.client.methods.HttpPost;
    import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
    import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
    import org.apache.http.conn.ssl.TrustStrategy;
    import org.apache.http.entity.StringEntity;
    import org.apache.http.impl.client.CloseableHttpClient;
    import org.apache.http.impl.client.HttpClients;
    import org.apache.http.ssl.SSLContexts;
    import org.apache.http.util.EntityUtils;
    import org.springframework.stereotype.Component;

    import javax.net.ssl.SSLContext;
    import java.io.File;
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;
    import java.nio.file.Files;
    import java.security.*;
    import java.security.cert.CertificateException;
    import java.util.Objects;

    @Slf4j
    @Component
    public class SSLHttpClientUtil {

    // 根据支持的TLS版本调整即可
    private static final String[] SUPPORT_TLS_VERSION = new String[]{&#34;TLSv1.2&#34;, &#34;TLSv1.3&#34;};
    // 
    private static final String PKCS12 = &#34;PKCS12&#34;;
    
    public String sendCommon(String json, String requestUrl) {
        KeyStore keyStore = null;
        try {
            keyStore = KeyStore.getInstance(PKCS12);
        } catch (KeyStoreException e) {
            throw new RuntimeException(e);
        }
        // 加载 cert 证书
        String certFilePath = &#34;&#34;;
        try {
            keyStore.load(Files.newInputStream(
                            new File(certFilePath).toPath()),
                    new char[]{&#39;1&#39;, &#39;2&#39;,&#39;3&#39;,&#39;4&#39;,&#39;5&#39;,&#39;6&#39;});
        } catch (IOException e) {
            log.warn(&#34;【SSLHttpClientUtil】读取文件失败,读取文件地址为:{}&#34;, certFilePath);
            throw new RuntimeException(e);
        } catch (NoSuchAlgorithmException e) {
            log.info(&#34;【SSLHttpClientUtil】算法异常 {}&#34;, ExceptionUtil.getMessage(e));
            throw new RuntimeException(e);
        } catch (CertificateException e) {
            log.info(&#34;【SSLHttpClientUtil】证书异常 {}&#34;, ExceptionUtil.getMessage(e));
            throw new RuntimeException(e);
        }
    
        SSLContext sslcontext = null;
        try {
            sslcontext = SSLContexts.custom()
                    //忽略掉对服务器端证书的校验
                    .loadTrustMaterial((TrustStrategy) (chain, authType) -&gt; true)
                    //加载服务端提供的truststore(如果服务器提供truststore的话就不用忽略对服务器端证书的校验了)
                    .loadTrustMaterial(new File(&#34;D:\\truststore.jks&#34;), &#34;123456&#34;.toCharArray(),
                            new TrustSelfSignedStrategy())
                    // 加载证书的部分放到单独的代码块(推荐)
    

    // .loadKeyMaterial(keyStore, acsConfig.getCertFilePassWord().toCharArray())
    .build();
    } catch (NoSuchAlgorithmException e) {
    log.info("【SSLHttpClientUtil】算法不存在 {}", ExceptionUtil.getMessage(e));
    throw new RuntimeException(e);
    } catch (KeyManagementException | KeyStoreException | CertificateException | IOException e) {
    log.info("【SSLHttpClientUtil】密钥管理异常 {}", ExceptionUtil.getMessage(e));
    throw new RuntimeException(e);
    }
    SSLConnectionSocketFactory sslConnectionSocketFactory =
    new SSLConnectionSocketFactory(
    sslcontext,
    SUPPORT_TLS_VERSION,
    null,
    SSLConnectionSocketFactory.getDefaultHostnameVerifier());

        CloseableHttpClient httpclient = HttpClients.custom()
                .setSSLSocketFactory(sslConnectionSocketFactory)
                .build();
        HttpEntity responseEntity = null;
        try {
    
            HttpPost httpPost = new HttpPost(requestUrl);
            httpPost.setHeader(&#34;Content-Type&#34;, &#34;application/json;charset=UTF-8&#34;);
            httpPost.setHeader(&#34;Accept&#34;, &#34;application/json&#34;);
            StringEntity stringEntity = new StringEntity(json, &#34;UTF-8&#34;);
            stringEntity.setContentType(&#34;text/json&#34;);
            httpPost.setEntity(stringEntity);
            log.info(&#34;【SSLHttpClientUtil】请求 URL 为:&#34; + requestUrl);
            if(log.isDebugEnabled()){
                log.debug(&#34;【SSLHttpClientUtil】请求行信息为:&#34; + httpPost.getRequestLine());
            }
            log.info(&#34;【SSLHttpClientUtil】请求体为::&#34; + json);
            CloseableHttpResponse response = null;
            response = httpclient.execute(httpPost);
            if(Objects.isNull(response)){
                throw new RuntimeException(&#34;response 返回内容为 null&#34;);
            }
            if(log.isDebugEnabled()){
                log.debug(&#34;【SSLHttpClientUtil】响应状态码 {}&#34;, response.getStatusLine());
            }
            responseEntity = response.getEntity();
            String resJson = IOUtils.toString(responseEntity.getContent(), StandardCharsets.UTF_8);
            log.info(&#34;【SSLHttpClientUtil】响应内容 {}&#34;, resJson);
            return resJson;
        } catch (ClientProtocolException e) {
            log.warn(&#34;【SSLHttpClientUtil】HTTP请求异常 {}&#34;, ExceptionUtil.getMessage(e));
            throw new RuntimeException(e);
        } catch (IOException e) {
            log.warn(&#34;【SSLHttpClientUtil】IOException {}&#34;, ExceptionUtil.getMessage(e));
            throw new RuntimeException(e);
        } finally {
            try {
                // 释放IO
                if(Objects.nonNull(responseEntity)){
                    EntityUtils.consume(responseEntity);
                }
    
            } catch (IOException e) {
                log.warn(&#34;【SSLHttpClientUtil】EntityUtils.consume 异常 {}&#34;, ExceptionUtil.getMessage(e));
                throw new RuntimeException(e);
            }
            try {
                if (Objects.nonNull(httpclient)) {
                    httpclient.close();
                }
            } catch (IOException e) {
                log.warn(&#34;【SSLHttpClientUtil】httpclient 关闭异常 {}&#34;, ExceptionUtil.getMessage(e));
                throw new RuntimeException(e);
            }
        }
    }
    

    }

    8. 实战使用请求工具类

    下面介绍如何将上面的工具类应用到个人日常工作流中。

    注意个人实践这里的环境前提,本地有一台配置了SSL证书的Linux服务器,开发过程中通过nginx进行请求转发,达到本地服务器和需要HTTPS的接口进行对接的目的。

    这部分配置是由运维搞定的,所以开发人员只需要关注支持SSL访问即可。

    8.1、生成SSL证书

    第一步是使用csr证书生成工具,在网站中填写外网地址,下载csr文件和key文件。

    生成SSL证书使用的是:Myssl

    image.png

    8.2、对csr文件进行签名

    下载之后,对csr文件进行签名

    省略.......

    8.3、执行相关命令

    将三个文件(csr文件、key文件、签名文件)放在一个文件夹中,在该文件夹打开cmd,按顺序执行以下命令

    代码语言:javascript
    复制
    openssl pkcs12 -export -out keystore.p12 -in signed-csr.crt -inkey private.key.PEM
    代码语言:javascript
    复制
    keytool -importkeystore -srckeystore keystore.p12 -srcstoretype PKCS12 -deststoretype JKS -destkeystore keystore.jks
    代码语言:javascript
    复制
    keytool -import -file ca-crt.pem -keystore truststore.jks

    9. 小结

    本教程讨论了如何为 Apache HttpClient 配置 SSL,以便它能够接收任何 HTTPS URL,而不受证书的影响。同时后面还演示了对 Spring RestTemplate 的相同配置。

    不过,这里需要注意,这些策略完全忽略了证书检查,因此并不安全,只能在合理的情况下使用。

    更低版本的Apache HttpClient 4可以查阅相关资料,这里就不做过多演示了。

    参考资料

    Apache HttpClient with SSL | Baeldung

    各种证书后缀介绍

    本部内容主要来自:SSL各种眼花迷乱的证书后缀_cer.36.ⅴip-CSDN博客

    项目要用Https,自然需要知名机构的SSL证书。这里记录一下让人头疼的各种证书后缀。

    (1) x.509 证书

    X.509是常见通用的证书格式,包含证书持有人的公钥,加密算法等信息。所有的证书都符合为Public Key Infrastructure (PKI) 制定的 ITU-T X509 国际标准。

    在Java代码中,我们可以使用下面的代码进行X509证书的生成。

    当然这个证书不能用在实际的生产项目里,因为没有权威CA的认证,会被浏览器认定为不安全

    代码语言:javascript
    复制

    private void generateX509(KeyPair keyPair) throws CertificateEncodingException, NoSuchAlgorithmException, SignatureException, NoSuchProviderException, InvalidKeyException, IOException {
    Security.addProvider(new BouncyCastleProvider());
    X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
    // Set the subject DN
    X500Principal dnName = new X500Principal("CN=Example");
    certGen.setSerialNumber(new BigInteger("2221323124151214214"));
    certGen.setSubjectDN(dnName);
    // Set the issuer DN
    certGen.setIssuerDN(dnName);
    // Set the public key
    certGen.setPublicKey(keyPair.getPublic());
    // Set the validity period
    certGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60 * 60 * 24 * 30));
    certGen.setNotAfter(new Date(System.currentTimeMillis() + 1000L * 60 * 60 * 24 * 365 * 10));
    // Set the signature algorithm
    certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
    // Generate the certificate
    X509Certificate cert = certGen.generate(keyPair.getPrivate(), "BC");

    if(!FileUtil.isExistingFile(new File("example.pem"))){
    saveX5092File(cert);
    }
    }

    /**

    • 将证书保存到磁盘
      /
      private void saveX5092File(X509Certificate cert) throws IOException, CertificateEncodingException {
      FileOutputStream fos = new FileOutputStream("example.pem");
      // 保存磁盘的时候需要添加头尾前缀后缀,加载之后注意删除头尾后缀。
      fos.write("-----BEGIN CERTIFICATE-----\n".getBytes());
      fos.write(Base64.getEncoder().encode(cert.getEncoded()));
      fos.write("\n-----END CERTIFICATE-----\n".getBytes());
      fos.close();
      }
      }

    (2)pkcs1-pkcs12

    公钥加密(非对称加密)的一种标准(Pbulic Key Cryptography Standards),一般存储为.pn*.p12是包含证书和密钥的封装格式。

    PKCS 全称是 Public-Key Cryptography Standards ,是由 RSA 实验室与其它安全系统开发商为促进公钥密码的发展而制订的一系列标准,PKCS 目前共发布过 15 个标准。

    常用的有:

    代码语言:javascript
    复制
    PKCS#7 Cryptographic Message Syntax Standard
    PKCS#10 Certification Request Standard
    PKCS#12 Personal Information Exchange Syntax Standard
    PKCS#7 常用的后缀是: .P7B .P7C .SPC

    PKCS#12 常用的后缀有: .P12 .PFX

    pfx/p12用于存放个人证书/私钥,他通常包含保护密码,2进制方式
    p10是证书请求
    p7r是CA对证书请求的回复,只用于导入
    p7b以树状展示证书链(certificate chain),同时也支持单个证书,不含私钥。

    PKCS#12 对应上面工具类代码 PKCS12

    (3) .der

    ASN.1是一套完整的数据结构与数据存储格式描述,BER/DER是ASN.1的二进制编码方式

    (4) *.pem</strong></h3><p> Privacy Enhanced Mail,证书或密钥的Base64文本存储格式,可以单独存放证书或密钥,也可以同时存放证书或密钥。

    .pem跟crt/cer的区别是它以Ascii来表示。

    也有说PEM是DER编码再进行Base64编码后的文本格式。

    公钥和私钥一般都是用PEM方式保存,但是公钥文件还不足以成为证书,还需要CA的签名;

    CSR是证书签名请求,CA用自己的私钥文件签名之后生成CRT文件就是完整的证书了。

    一般 Apache 和 Nginx 服务器应用偏向于使用 PEM 这种编码格式。

    (5).key

    单独存放的pem格式的密钥,一般保存为*.key。

    (6).cer .crt

    两个指的都是证书。windows下叫cer,linux下叫crt;存储格式可以为pem也可以为der。.cer/.crt是用于存放证书,它是2进制形式存放的,不含私钥。

    (7) .csr

    证书签名请求(Certificate sign request),包含证书持有人的信息,如国家,邮件,域名等。

    (8) .pfx

    微软iis的实现。用于存放个人证书/私钥,通常包含保护密码,2进制方式

    (9) .jks

    Java Key Store–Java 的 Keytool实现的证书格式。

    (9) .crl

    证书吊销列表(Certificate Revocation List)。

    使用openssl进行证书转换:

    Reference

    [1]

    Cool basic and more advanced things you can do with the HttpClient 4: https://www.baeldung.com/httpclient-guide