掘金 后端 ( ) • 2024-04-28 17:07

theme: channing-cyan highlight: a11y-dark

早上刚来,就看到仓库那边不停发消息说,我们的某个功能用不了了。赶紧放下早餐加紧看。

原来是调的一个三方接口报错了:

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.Alert.createSSLException(Alert.java:131)
	at sun.security.ssl.TransportContext.fatal(TransportContext.java:353)
	at sun.security.ssl.TransportContext.fatal(TransportContext.java:296)
	at sun.security.ssl.TransportContext.fatal(TransportContext.java:291)
	at sun.security.ssl.CertificateMessage$T12CertificateConsumer.checkServerCerts(CertificateMessage.java:652)
	at sun.security.ssl.CertificateMessage$T12CertificateConsumer.onCertificate(CertificateMessage.java:471)
	at sun.security.ssl.CertificateMessage$T12CertificateConsumer.consume(CertificateMessage.java:367)
	at sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:376)
	at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:444)
	at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:422)
	at sun.security.ssl.TransportContext.dispatch(TransportContext.java:183)
	at sun.security.ssl.SSLTransport.decode(SSLTransport.java:154)
	at sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1279)
	at sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1188)
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:401)
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:373)

查看原因:

由于JVM默认信任证书不包含该目标网站的SSL证书,导致无法建立有效的信任链接。

奥...原来是他们把接口从http改为了https,导致我们获取数据报错了。再看看他们的证书,奥...新的。

image.png

好了,看看我们的逻辑,这其实是一个获取对方生成的PDF文件的接口

PdfReader pdfReader = new PdfReader(url);

url就是他们给的链接,是这行代码报的错。这时候,开始研究,在网上扒拉,找到了初版方案

尝试1

写一个程序专门获取安全证书,这代码有点长,全贴出来影响阅读。我给扔我hithub上了https://github.com/lukezhao6/InstallCert/blob/main/InstallCert.java 将这个文件贴到本地,执行javac InstallCert.java将其进行编译

image.png

编译完长这样:

image.png 然后再执行java InstallCert www.baidu.com (这里我们用百度举例子,实际填写的就是你想要获取证书的目标网站)

image.png 报错不用怕,因为它会去检查目标服务器的证书,如果出现了SSLException,表示证书可能存在问题,这时候会把异常信息打印出来。

在生成的时候需要输入一个1

image.png 这样,我们需要的证书文件就生成好了

image.png

这时候,将它放入我们本地的 jdk的lib\security文件夹内就行了

image.png

重启,这时候访问是没有问题了。阶段性胜利。

但是,但是。一顿操作下来,对于测试环境的docker,还有生产环境貌似不能这么操作。 放这个证书文件比较费事。

那就只能另辟蹊径了。

尝试2

搜到了,还有两种方案。

1.通过System.setProperty("javax.net.ssl.trustStore", "你的jssecacerts证书路径");

2.程序启动命令-Djavax.net.ssl.trustStore=你的jssecacerts证书路径 -Djavax.net.ssl.trustStorePassword=changeit

我尝试了第一种,System.setProperty可以成功,但是读不到文件,权限什么的都是ok的。 检查了蛮多地方

  • 路径格式问题
  • 文件是否存在
  • 文件权限
  • 信任库密码
  • 系统属性优先级

貌似都是没问题的,但肯定又是有问题的,因为没起作用。但是想着这样的接口有4个,万一哪天其他三个也改了,我又得来一遍。所以就算研究出来了,还是不能稳坐钓鱼台。有没有一了百了的方法嘞。

尝试3

还真找到了:这个错是因为对方网站的证书不被java信任么,那咱不校验了,直接全部信任。这样就算其他接口改了,咱也不愁。而且这个就是获取pdf,貌似安全性没那么重。那就开搞。

代码贴在了下方,上边的大概都能看懂吧,下方的我加了注释。

URL console = new URL(url);
HttpURLConnection conn = (HttpURLConnection) console.openConnection();
if (conn instanceof HttpsURLConnection)  {
    SSLContext sc = SSLContext.getInstance("SSL");
    sc.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new java.security.SecureRandom());
    ((HttpsURLConnection) conn).setSSLSocketFactory(sc.getSocketFactory());
    ((HttpsURLConnection) conn).setHostnameVerifier(new TrustAnyHostnameVerifier());
}
conn.connect();
InputStream inputStream = conn.getInputStream();
PdfReader pdfReader = new PdfReader(inputStream);
inputStream.close();
conn.disconnect();
private static class TrustAnyTrustManager implements X509TrustManager {
    //这个方法用于验证客户端的证书。在这里,方法体为空,表示不对客户端提供的证书进行任何验证。
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
    }
    //这个方法用于验证服务器的证书。同样,方法体为空,表示不对服务器提供的证书进行任何验证。
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
    }
    //这个方法返回一个信任的证书数组。在这里,返回空数组,表示不信任任何证书,也就是对所有证书都不做任何信任验证。
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[]{};
    }
}
//这个方法用于验证主机名是否可信。在这里,无论传入的主机名是什么,方法始终返回 true,表示信任任何主机名。这就意味着对于 SSL 连接,不会对主机名进行真实的验证,而是始终接受所有主机名。
private static class TrustAnyHostnameVerifier implements HostnameVerifier {
    public boolean verify(String hostname, SSLSession session) {
        return true;
    }
}

解决了解决了,这样改算是个比较不错的方案了吧。