HttpClient实现HTTPS客户端编程---可信证书与自签名证书

HttpClient的HTTPS客户端编程—可信证书与自签名证书

本文基于HttpClient4.5.4,对可信证书和自签名证书的网站访问编码,涉及https连接过程、证书、证书链、根证书、keystore、自签名等概念,就不在本文中细说了。

  • HttpClient的HTTPS客户端编程可信证书与自签名证书
    • 简介
      • 前世今生
      • 如何引入
    • 如何发送HTTPS请求
      • 问题一访问非可信证书网站会如何例如12306
      • 问题二在访问百度时SSL握手过程中百度网站的证书是如何被认为是可信证书的
      • 问题三服务器就是要用自签名证书如何解决
      • 问题四为什么会有匹配不到AlternativeName的错误
      • 问题五为什么我设置了ConnectionManager后又不行了呢
    • 总结
      • httpsSSL证书校验常见的几点
      • 最终的完整代码


简介

前世今生

网上能搜到httpclient使用的各种写法,基本是由于版本更迭导致的,前期为Apache Commons HttpClient,现在是Apache HttpComponents,从4.3.x版本开始类名和调用方式相较早期版本有了明显变化,下文中所有代码基于当前最新的4.5.4版本。

The Commons HttpClient project is now end of life, and is no longer being developed. It has been replaced by the Apache HttpComponents project in its HttpClient and HttpCore modules, which offer better performance and more flexibility. ——[来自官网]

如何引入

<dependencies>
    <dependency>
        <groupId>org.apache.httpcomponentsgroupId>
        <artifactId>httpclientartifactId>
        <version>4.5.4version>
    dependency>
dependencies>

如何发送HTTPS请求

package org.fst.network;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

public class HttpGetTest {
    public static void main(String[] args) {

        CloseableHttpClient httpClient = HttpClients.createDefault();
        try {
            HttpGet httpGet = new HttpGet("http://www.baidu.com");
            System.out.println("Executing request " + httpGet.getRequestLine());
            CloseableHttpResponse response = httpClient.execute(httpGet);
            System.out.println("----------------------------------------");
            System.out.println(response.getStatusLine());
            System.out.println(EntityUtils.toString(response.getEntity()));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                httpClient.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果如下
HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第1张图片

地址改为https://www.baidu.com会如何呢,仍然能得到200 OK响应:
HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第2张图片

问题一:访问非可信证书网站会如何,例如12306?

访问地址改为https://www.12306.cn,运行效果如下,大家都知道12306证书非法吧哈哈:
HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第3张图片

问题二:在访问百度时,SSL握手过程中百度网站的证书是如何被认为是可信证书的?

很多人会不假思索的回答:“因为百度网站使用了可信CA签发的证书”。通过Chrome的开发者工具:F12->security可以看出来,百度的证书被浏览器认定为可信的。
HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第4张图片

而12306被认为不可信:
HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第5张图片

但我们要知道,浏览https网站的场景是由客户端校验服务器是否可信的(这里指一般的场景,当然还有双向认证),单从服务器如何如何并不能解释客户端的行为,真正的答案是windows系统预制了一批可信根证书,从Internet选项->内容->证书里面可以看到,如下图,有兴趣的可以找找是否有baidu的根证书:
HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第6张图片

类比浏览器,HttpClient既然能访问百度成功,其必然也加载了可信根证书作为判断依据,那么这些根证书在哪,由谁去加载的?通过debug代码,可以发现,java程序在默认的SSLSocketFactory中加载了104个证书(不同版本jdk证书数目可能有差别):
HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第7张图片

进一步在%JAVA_HOME%/jre/lib/security的目录下找到了疑似的keystore文件。
HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第8张图片
keytool -list -keystore cacerts -storepass changeit看一下,里面确实是这104个根证书。
HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第9张图片

问题三:服务器就是要用自签名证书,如何解决?

在维持整个校验过程不变的前提下,keystore中导入这个证书,将其认为可信即可。
第一步:将证书保存到本地:
Chrome浏览器F12-Security-View certificate打开证书信息窗口-详细信息-复制到文件将其保存为X.509格式。
证书窗口中我们可以看到证书链,保存链里面的任一个证书都可以。
HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第10张图片
第二步:将证书导入keystore(导入jdk中的caserts文件或者生成一个新的keystore文件)
一般不建议随意修改jdk中的文件,咱们生成一个新的keystore,并修改代码加载它。

keytool -import -keystore my.keystore -storepass 123456 -file 12306root.cer -alias 12306root

上文的代码也需要一些修改,给CloseableHttpClient对象设置自定义的SSLConnectionSocketFactory:

// 加载自定义的keystore
SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(new File("D:/java/IdeaProjects/test/src/main/resources/certs/my.keystore"), "123456".toCharArray()).build();
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext);
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslConnectionSocketFactory).build();

HttpGet httpGet = new HttpGet("https://www.12306.cn");
System.out.println("Executing request " + httpGet.getRequestLine());
CloseableHttpResponse response = httpClient.execute(httpGet);
System.out.println("----------------------------------------");
System.out.println(response.getStatusLine());
System.out.println(EntityUtils.toString(response.getEntity()));

与问题一中的结果对比,异常变了,从未找到合法证书变为未匹配到subject alternative names(可选名称)
HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第11张图片

问题四:为什么会有匹配不到AlternativeName的错误?

我们来看看源码中这段校验逻辑:服务器证书信息中有AlternativeName就用它和访问的地址比较,没有就用CN和访问的地址比较。
HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第12张图片

这俩东东分别对应证书(以百度的证书为例)中这两段信息:
HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第13张图片

而12306的证书没有可选名称,只有CN,且CN值为kyfw.12306.cn,咱们访问的是12306.cn,自然匹配不到了。
HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第14张图片

这有点尴尬了,12306的证书除了是自签名以外,证书颁给的域名还不对,怎么办呢,我们可以在构造SSLConnectionSocketFactory时重写域名校验逻辑,简单起见就直接校验通过返回true了(注意这是不得已而为之的办法,违反了证书的安全机制)

SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new HostnameVerifier() {
                public boolean verify(String s, SSLSession sslSession) {
                    // 我们可以重写域名校验逻辑,这里直接返回成功
                    return true;
                }
            });

HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第15张图片

问题五:为什么我设置了ConnectionManager后又不行了呢

一般代码中,我们会给HttpClient设置连接池:

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(20);
connectionManager.setDefaultMaxPerRoute(20);

httpClient = HttpClients.custom()
            .setSSLSocketFactory(sslConnectionSocketFactory)
            .setConnectionManager(connectionManager)
            .build();

结果辛苦调通的12306又访问不了了,回到了最初的问题:unable to find valid certification path to requested target。
阅读一番源码后,直接将原因告诉大家:
HttpClient实现HTTPS客户端编程---可信证书与自签名证书_第16张图片
如上图,HttpClient在connect()时,获取到的SSLSocketFactory,是new PoolingHttpClientConnectionManager()时默认构造的,并不是我们.setSSLSocketFactory(sslConnectionSocketFactory)设置的那一个。除非我们在new PoolingHttpClientConnectionManager就注册好传给它的构造函数。

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(RegistryBuilder.create()
                    .register("http", PlainConnectionSocketFactory.getSocketFactory())
                    .register("https", sslConnectionSocketFactory)
                    .build());

总结

https(SSL)证书校验常见的几点

  • 证书是否为可信CA签发
  • 证书中的AlternativeNames或CN是否与我们访问的地址相同
  • 证书是否过期/是否已被撤销(见CRL)

最终的完整代码

package org.fst.network;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import java.io.File;

public class HttpGetTest {
    public static void main(String[] args) {

        CloseableHttpClient httpClient = null;

        try {

            // 加载自定义的keystore
            SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(new File("D:/java/IdeaProjects/test/src/main/resources/certs/my.keystore"), "123456".toCharArray()).build();

            // 默认的域名校验类为DefaultHostnameVerifier,比对服务器证书的AlternativeName和CN两个属性。
            // 如果服务器证书这两者不合法而我们又必须让其校验通过,则可以自己实现HostnameVerifier。
            SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new HostnameVerifier() {
                public boolean verify(String s, SSLSession sslSession) {
                    // 我们可以重写域名校验逻辑
                    return true;
                }
            });

            // 一个httpClient对象对于https仅会选用一个SSLConnectionSocketFactory
            // 至少在4.5.34.5.4中,如果给HttpClient对象设置ConnectionManager,我们必须在PoolingHttpClientConnectionManager的构造方法中传入Registry,
            // 并将https对应的工厂设置为我们自己的SSLConnectionSocketFactory对象,因为在DefaultHttpClientConnectionOperator.connect()中,逻辑是从这里找SSLConnectionSocketFactory的。
            PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(RegistryBuilder.create()
                    .register("http", PlainConnectionSocketFactory.getSocketFactory())
                    .register("https", sslConnectionSocketFactory)
                    .build());
            connectionManager.setMaxTotal(20);
            connectionManager.setDefaultMaxPerRoute(20);

            httpClient = HttpClients.custom()
                    // 不在connectionManager中注册,仅在这里设置SSLConnectionSocketFactory是无效的,详见build()内部逻辑,在connectionManager不为null时,不会使用里的SSLConnectionSocketFactory
                    .setSSLSocketFactory(sslConnectionSocketFactory)
                    .setConnectionManager(connectionManager)
                    .build();

            HttpGet httpGet = new HttpGet("https://www.12306.cn");
            System.out.println("Executing request " + httpGet.getRequestLine());
            CloseableHttpResponse response = httpClient.execute(httpGet);
            System.out.println("----------------------------------------");
            System.out.println(response.getStatusLine());
            System.out.println(EntityUtils.toString(response.getEntity()));

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != httpClient)
                {
                    httpClient.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

你可能感兴趣的:(网络编程,httpclient,https,证书,自签名证书)