一、关于 Java 里的证书
上面所介绍的是浏览器对证书进行验证的过程,浏览器保存了一个常用的 CA 证书列表,在验证证书链的有效性时,直接使用保存的证书里的公钥进行校验,如果在证书列表中没有找到或者找到了但是校验不通过,那么浏览器会警告用户,由用户决定是否继续。与此类似的,操作系统也一样保存有一份可信的证书列表,譬如在 Windows 系统下,你可以运行 certmgr.msc 打开证书管理器查看,这些证书实际上是存储在 Windows 的注册表中,一般情况下位于:\SOFTWARE\Microsoft\SystemCertificates\ 路径下。那么在 Java 程序中是如何验证证书的呢?
和浏览器和操作系统类似,Java 在 JRE 的安装目录下也保存了一份默认可信的证书列表,这个列表一般是保存在 $JRE/lib/security/cacerts 文件中。要查看这个文件,可以使用类似 KeyStore Explorer 这样的软件,当然也可以使用 JRE 自带的 keytool 工具(后面再介绍),cacerts 文件的默认密码为 changeit (但是我保证,大多数人都不会 change it)。
我们知道,证书有很多种不同的存储格式,譬如 CA 在发布证书时,常常使用 PEM 格式,这种格式的好处是纯文本,内容是 BASE64 编码的,证书中使用 “-----BEGIN CERTIFICATE-----” 和 “-----END CERTIFICATE-----” 来标识。另外还有比较常用的二进制 DER 格式,在 Windows 平台上较常使用的 PKCS#12 格式等等。当然,不同格式的证书之间是可以相互转换的
二、Java中不同类型的密钥库(Keystore) 、
在 Java 平台下,证书常常被存储在 KeyStore 文件中,上面说的 cacerts 文件就是一个 KeyStore 文件,KeyStore 不仅可以存储数字证书,还可以存储密钥,存储在 KeyStore 文件中的对象有三种类型:Certificate、PrivateKey 和 SecretKey 。Certificate 就是证书,PrivateKey 是非对称加密中的私钥,SecretKey 用于对称加密,是对称加密中的密钥。KeyStore 文件根据用途,也有很多种不同的格式:JKS、JCEKS、PKCS12、DKS 等等,PixelsTech 上有一系列文章对 KeyStore 有深入的介绍,可以学习下:Different types of keystore in Java 。
-
JKS,Java Key Store。可以参见sun.security.provider.JavaKeyStore类,此密钥库是特定于Java平台的,通常具有jks的扩展名。此类型的密钥库可以包含私钥和证书,但不能用于存储密钥。由于它是Java特定的密钥库,因此不能在其他编程语言中使用。存储在JKS中的私钥无法在Java中提取。关于JKS的详细介绍可以参考
-
JCEKS,JCE密钥库(Java Cryptography Extension KeyStore)。可以认为是增强式的JKS密钥库,支持更多算法。可以参考com.sun.crypto.provider.JceKeyStore类,此密钥库具有jceks的扩展名。可以放入JCEKS密钥库的条目是私钥,密钥和证书。此密钥库通过使用Triple DES加密为存储的私钥提供更强大的保护。
JCEKS的提供者是SunJCE,它是在Java 1.4中引入的。因此,在Java 1.4之前,只能使用JKS。
-
PKCS12,一种标准的密钥库类型,可以在Java和其他语言中使用。可以参考sun.security.pkcs12.PKCS12KeyStore类。它通常具有p12或pfx的扩展名。可以在此类型上存储私钥,密钥和证书。与JKS不同,PKCS12密钥库上的私钥可以用Java提取。此类型是可以与其他语言(如C,C ++或C#)编写的其他库一起使用。
目前,Java中的默认密钥库类型是JKS,即如果在使用keytool创建密钥库时未指定-storetype,则密钥库格式将为JKS。但是,默认密钥库类型将在Java 9中更改为PKCS12,因为与JKS相比,它具有增强的兼容性。可以在$ JRE / lib / security / java.security文件中检查默认密钥库类型。
三、KeyStore 和 TrustStore
到目前为止,我们所说的 KeyStore 其实只是一种文件格式而已,实际上在 Java 的世界里 KeyStore 文件分成两种:KeyStore 和 TrustStore,这是两个比较容易混淆的概念。
不过这两个东西从文件格式来看其实是一样的。KeyStore 保存私钥,用来加解密或者为别人做签名;TrustStore 保存一些可信任的证书,访问 HTTPS 时对被访问者进行认证,以确保它是可信任的。所以准确来说,上面的 **cacerts 文件应该叫做 TrustStore **而不是 KeyStore,只是它的文件格式是 KeyStore 文件格式罢了。
除了 KeyStore 和 TrustStore ,Java 里还有两个类 KeyManager 和 TrustManager 与此息息相关。JSSE 的参考手册中有一张示意图,说明了各个类之间的关系:
![](https://img-blog.csdnimg.cn/img_convert/b6906a79ad2a8068c87a88cf8d8a3851.webp?x-oss-process=image/format,png#averageHue=#e3dcb7&clientId=u431af540-a2ee-4&crop=0&crop=0&crop=1&crop=1&errorMessage=unknown error&from=paste&id=u00f404d8&margin=[object Object]&originHeight=422&originWidth=581&originalType=url&ratio=1&rotation=0&showTitle=false&status=error&style=none&taskId=ue5fdd832-b618-4fa2-b52f-8f617475a7d&title=)
可以看出如果要进行 SSL 会话,必须得新建一个 SSLSocket 对象,而 SSLSocket 对象是通过 SSLSocketFactory 来管理的,SSLSocketFactory 对象则依赖于 SSLContext ,SSLContext 对象又依赖于 keyManager、TrustManager 和 SecureRandom。我们这里最关心的是 TrustManager 对象,另外两个暂且忽略,因为正是 TrustManager 负责证书的校验,对网站进行认证,要想在访问 HTTPS 时通过认证,不报 sun.security.validator.ValidatorException 异常,必须从这里开刀。
四、自定义 TrustManager 绕过证书检查
我们知道了 TrustManager 是专门负责校验证书的,那么最容易想到的方法应该就是改写 TrustManager 类,让它不要对证书做校验,这种方法虽然粗暴,但是却相当有效,而且 Java 中的 TrustManager 也确实可以被重写,下面是示例代码:
public static void main(String[] args) throws Exception {String url = "https://kyfw.12306.cn/otn/";// Create a trust manager that does not validate certificate chainsTrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() {public X509Certificate[] getAcceptedIssuers() {return null;}public void checkClientTrusted(X509Certificate[] certs, String authType) {// don't check}public void checkServerTrusted(X509Certificate[] certs, String authType) {// don't check}}};SSLContext ctx = SSLContext.getInstance("TLS");ctx.init(null, trustAllCerts, null);LayeredConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(ctx);CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslSocketFactory).build();HttpGet request = new HttpGet(url);request.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) ...");CloseableHttpResponse response = httpclient.execute(request);String content = EntityUtils.toString(response.getEntity(), "UTF-8");System.out.println(content);
}
五、使用证书
对于有些证书,我们基本上确定是可以信任的,但是这些证书又不在 Java 的 cacerts 文件中,譬如 12306 网站,或者使用了 Let’s Encrypt 证书的一些网站,对于这些网站,我们可以将其添加到信任列表中,而不是使用上面的方法统统都相信,这样程序的安全性仍然可以得到保障。
5.1 使用 keytool 导入证书
简单的做法是将这些网站的证书导入到 cacerts 文件中,这样 Java 程序在校验证书的时候就可以从 cacerts 文件中找到并成功校验这个证书了。上面我们介绍过 JRE 自带的 keytool 这个工具,这个工具小巧而强悍,拥有很多功能。首先我们可以使用它查看 KeyStore 文件,使用下面的命令可以列出 KeyStore 文件中的所有内容(包括证书、私钥等):
$ keytool -list -keystore cacerts
然后通过下面的命令,将证书导入到 cacerts 文件中:
$ keytool -import -alias 12306 -keystore cacerts -file 12306.cer
要想将网站的证书导入 cacerts 文件中,首先要获取网站的证书,譬如上面命令中的 12306.cer 文件,它是使用浏览器的证书导出向导保存的。如下图所示:
![](https://img-blog.csdnimg.cn/img_convert/19d15f3b2ad344c08c9bd2edd7a24000.webp?x-oss-process=image/format,png#clientId=u431af540-a2ee-4&crop=0&crop=0&crop=1&crop=1&errorMessage=unknown error&from=paste&id=ua94f44cc&margin=[object Object]&originHeight=761&originWidth=1299&originalType=url&ratio=1&rotation=0&showTitle=false&status=error&style=none&taskId=uf98f6132-d8f8-4187-8dd1-cb7abe27e1a&title=)
关于 keytool 的更多用法,可以参考 keytool 的官网手册,SSLShopper 上也有一篇文章列出了常用的 keytool 命令。
5.2 使用 KeyStore 动态加载证书
使用 keytool 导入证书,这种方法不仅简单,而且保证了代码的安全性,最关键的是代码不用做任何修改。所以我比较推荐这种方法。但是这种方法有一个致命的缺陷,那就是你需要修改 JRE 目录下的文件,如果你的程序只是在自己的电脑上运行,那倒没什么,可如果你的程序要部署在其他人的电脑上或者公司的服务器上,而你没有权限修改 JRE 目录下的文件,这该怎么办?如果你的程序是一个分布式的程序要部署在成百上千台机器上,难道还得修改每台机器的 JRE 文件吗?好在我们还有另一种通过编程的手段来实现的思路,在代码中动态的加载 KeyStore 文件来完成证书的校验,抱着知其然知其所以然的态度,我们在最后也实践下这种方法。通过编写代码可以更深刻的了解 KeyStore、TrustManagerFactory、SSLContext 以及 SSLSocketFactory 这几个类之间的关系。
@Test
public void basicHttpsGetUsingSslSocketFactory() throws Exception {String keyStoreFile = "D:\\code\\ttt.ks";String password = "poiuyt";KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());FileInputStream in = new FileInputStream(keyStoreFile);ks.load(in, password.toCharArray());System.out.println(KeyStore.getDefaultType().toString());System.out.println(TrustManagerFactory.getDefaultAlgorithm().toString());TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());tmf.init(ks);SSLContext ctx = SSLContext.getInstance("TLS");ctx.init(null, tmf.getTrustManagers(), null);LayeredConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(ctx);String url = "https://ttt.aneasystone.com";/*** Return the page with content:* 401 Authorization Required*/CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslSocketFactory).build();HttpGet request = new HttpGet(url);request.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) ...");CloseableHttpResponse response = httpclient.execute(request);String responseBody = readResponseBody(response);System.out.println(responseBody);
}
最后的最后,我们还可以通过下面的属性来指定 trustStore ,这样也不需要编写像上面那样大量繁琐的代码,另外,参考我前面的博客,这些属性还可以通过 JVM 的参数来设置。
System.setProperty("javax.net.ssl.trustStore", "D:\\code\\ttt.ks");
System.setProperty("javax.net.ssl.trustStorePassword", "poiuyt");
六、参考
本文链接:https://my.lmcjl.com/post/9120.html
4 评论