背景
要使用Apache HTTP Client-4.5.2做一些连接;但是有的域名是使用的自签证书,有的是CA签发证书;同时又不想跳过证书验证,且使用一个HTTPClient。
如果单单实现验证只使用自签证书或者只使用CA证书的域名比较简单;前者只需要new loadTrustMaterial(File file, char[] storePassword)
.
Java Key Store 与 HTTPS
首先需要明白jks是什么,HTTPS连接建立过程。
- jks就是Java KeyStore,KeyStore是个啥东西。不同场景下意义是不同的;在HTTPClient指双向证书验证时需要这个(
loadKeyStoreManager
),含有公钥和私钥,KeyStore一般是用密码保护的,私钥本身自己也是一个"密码".
https://stackoverflow.com/questions/23202046/what-is-keystore - 数字签名技术:CA使用自己的证书私钥加密一个普通企业或机构的一个公钥,这个过程就是签名,签名的结果就是这个企业域名(或软件)的数字证书,服务器把这个东西下发给浏览器,浏览器使用预置在电脑系统的根证书或者浏览器自己的证书库,即CA的根证书,用它们的公钥去验证发过来的数字证书是否是CA为这个域名签发的,数字证书签发和验证这一过程用到非对称密钥加密。验证通过,浏览器安全地随机一些字符并使用发过来的证书的公钥进行加密,服务器收到后用它的私钥进行解密;然后服务器和浏览器之间就使用这个字符来加密它们之间的流量,这个是对称密钥加密,即加密和解密用一样的密钥或者可以相互计算出来。
大致的流程是这样,有些不太严谨,比如根证书验证数字签名的方法是加密服务器发过来的消息摘要和它自己带过来的签名是否一致,为何需要消息摘要呢(比如MD5)?因为RSA的同态性。 - TLS是SSL的演进版本,SSL有很多Bug都是基于运输层的安全协议。https://www.differencebetween.com/difference-between-ssl-and-vs-tls/
知道了这些,基本可以明白,可以使用签发自签证书的根证书的公钥来验证服务器发送过来的自签证书了。
从keyStore中取出根证书
从KeyStore中取出根证书,如果你不知道根证书的别名,可以遍历 keyStore.aliases()
得出来。https://stackoverflow.com/questions/26711731/read-public-key-from-file-in-keystore
public static void init() throws Exception {
rootCerStream = new FileInputStream(loadResourceFile(KEY_STORE_FILE_NAME));
keyStore = KeyStore.getInstance("JKS");
keyStore.load(rootCerStream,KEY_STORE_PASSWORD.toCharArray());
rootCer = keyStore.getCertificate(ROOT_CER_ALIAS);
}
用自己的根证书校验
获取一个SSLContext
来给HTTPClientBuilder
来setSSLContext
,因为需要SSLConnectionSocketFactory
来创建SSL socket
public static SSLContext getMixSSLContext() throws Exception{
SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(null, new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
for (X509Certificate cer : chain){
try {
cer.verify(rootCer.getPublicKey());
} catch (Exception e){
continue;
}
return true;
}
return false;
//如果不想验证证书,直接return true就行了
}
}).build();
return sslContext;
}
同时有些时候,可能需要将 builder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
来跳过证书的域名校验。 比如你直接访问了IP,或者你直接访问了ELB而不是那个签发域名,就会提示错误。当然安全的写法是使用new DefaultHostnameVerifier()
PS:感觉还是SSLContextBuilder相当方便
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
....
builder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
builder.setSSLHostnameVerifier(new DefaultHostnameVerifier());
其他办法
还有一种解决方法就是将自签证书的根证书预置到Java的CACert中
https://stackoverflow.com/questions/11143360/ssl-certificate-verification-in-java
https://hc.apache.org/httpcomponents-client-4.5.x/current/httpclient/apidocs/中看SSLConnectionSocketFactory的文档,告诉怎么把根证书导入JDK的KeyStore.
keytool -import -file caroot.crt -keystore lib\security\cacerts
可能的问题
-
按照上述操作还提示证书验证不通过,可能是因为你使用了
connectionManager
,具体看这里builder的setSSLContext
的注释:https://github.com/apache/httpcomponents-client/blob/4.5.x/httpclient/src/main/java/org/apache/http/impl/client/HttpClientBuilder.java#L308
需要在构造connectionManager时就传入自定义的SSLConnectionSocketFactory
https://github.com/apache/httpcomponents-client/blob/4.5.x/httpclient/src/main/java/org/apache/http/impl/conn/PoolingHttpClientConnectionManager.java#L130 -
对称加密算法常用的有RC4,DES,AES。
RC4
具有前向安全性,后两者没有;前向安全意味着,即使你有根证书私钥,抓包HTTPS流量后无法解密通信内容,就是说,谁也解密不了了。比如csdn.net使用的是TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
对称加密算法是AES加密,抓包后csdn公司的人可解密,文心一言如是说:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 是一个结合了ECDHE密钥交换、RSA身份验证、AES-128对称加密、GCM工作模式和SHA-256哈希算法的TLS密码套件,旨在提供安全、可靠的网络通信。
了解些背景知识,还有源码,仔细阅读源码,也不是很难的。