【编程问题】SunCertPathBuilderException: unable to find valid certification path to requested target

SunCertPathBuilderException: unable to find valid certification path to requested target

系统:Win10
Java:1.8.0_351
IDEA:2022.3.3

1.问题描述

在一个项目中,本来需要使用爬虫获取国家的省市区数据(https://www.mca.gov.cn/mzsj/xzqh/2022/202201xzqh.html),结果发现在使用 URLConnection 获取链接的时候,报了如下错误

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:370)
	at sun.security.ssl.TransportContext.fatal(TransportContext.java:313)
	at sun.security.ssl.TransportContext.fatal(TransportContext.java:308)
	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:479)
	at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:457)
	at sun.security.ssl.TransportContext.dispatch(TransportContext.java:200)
	at sun.security.ssl.SSLTransport.decode(SSLTransport.java:155)
	at sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1320)
	at sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1233)
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:417)
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:389)
	at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:558)
	at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:201)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1584)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1512)
	at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:268)
	at com.lijinjiang.crawler.GetAreaData.getHtml(GetAreaData.java:51)
	at com.lijinjiang.crawler.GetAreaData.main(GetAreaData.java:31)
Caused by: 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.validator.PKIXValidator.doBuild(PKIXValidator.java:439)
	at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:306)
	at sun.security.validator.Validator.validate(Validator.java:271)
	at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:312)
	at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:221)
	at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:128)
	at sun.security.ssl.CertificateMessage$T12CertificateConsumer.checkServerCerts(CertificateMessage.java:636)
	... 18 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141)
	at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126)
	at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:280)
	at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:434)
	... 24 more

2.问题解决

经过一番查询资料之后,我明白了这是因为我们客户端缺乏该网站的 SSL 证书的问题,所以只需要将该网站 SSL 证书 导入到我们客户端的 jssecacerts 认证库中,经过测试封装后,我使用下面的代码可以运行直接安装信任证书(不同网站只需要修改代码中的 hostname 值即可)

import javax.net.ssl.*;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Enumeration;

public class InstallCert {
    public static final String path = System.getProperty("java.home") + File.separator + "lib" + File.separator + "security";//Java秘钥库存放位置

    public static void main(String[] args) throws Exception {
        String hostname = "www.mca.gov.cn";//网站地址
        String passphrase = "changeit";//信任证书默认密钥changeit
        createCert(hostname, passphrase);//创建证书
    }

    public static void createCert(String hostname, String passphrase) throws Exception {
        String host;
        int port;
        char[] passphrases;
        if (hostname != null && !hostname.isEmpty()) {
            String[] c = hostname.split(":");
            host = c[0];
            port = (c.length == 1) ? 443 : Integer.parseInt(c[1]);
            passphrases = passphrase.toCharArray();
        } else {
            System.out.println("请正确输入服务器地址: <host>[:port]");
            return;
        }

        System.out.println("加载秘钥存储库数据");

        File certs = new File(path, "cacerts");
        InputStream certsInputStream = null;
        if (certs.exists() && certs.isFile()) {
            certsInputStream = Files.newInputStream(certs.toPath());
        }

        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(certsInputStream, passphrases);
        if (certsInputStream != null) {
            certsInputStream.close();
        }

        File file = new File(path, "jssecacerts");
        InputStream inputStream = null;
        if (file.exists()) {
            if (!file.isFile()) {
                System.out.println("存在文件夹: " + file.getPath());
                return;
            } else {
                if (file.length() > 0) {
                    inputStream = Files.newInputStream(file.toPath());
                }
            }
        }
        KeyStore customKs = KeyStore.getInstance(KeyStore.getDefaultType());
        customKs.load(inputStream, passphrases);
        if (inputStream != null) {
            inputStream.close();
        }

        //将jssecacerts中证书全部加载到keyStore库中
        Enumeration<String> aliases = customKs.aliases();
        if (aliases != null) {
            while (aliases.hasMoreElements()) {
                String alias = aliases.nextElement();
                Certificate certificate = customKs.getCertificate(alias);
                keyStore.setCertificateEntry(alias, certificate);
            }
        }

        SSLContext context = SSLContext.getInstance("TLS");
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(keyStore);
        X509TrustManager defaultTrustManager = (X509TrustManager) tmf.getTrustManagers()[0];
        SavingTrustManager tm = new SavingTrustManager(defaultTrustManager);
        context.init(null, new TrustManager[]{tm}, null);
        SSLSocketFactory factory = context.getSocketFactory();

        System.out.println("打开链接 " + host + ":" + port);
        SSLSocket socket = (SSLSocket) factory.createSocket(host, port);
        socket.setSoTimeout(10000);
        try {
            System.out.println("开始SSL握手......");
            socket.startHandshake();
            socket.close();
            System.out.println("没有错误,证书已受信任");
            return;
        } catch (SSLException e) {
            System.out.println();
            System.out.println("SSL握手失败: " + e.getMessage());
        }

        X509Certificate[] chain = tm.chain;
        if (chain == null) {
            System.out.println("无法获取服务器证书链");
            return;
        }

        System.out.println();
        System.out.println("服务器发送了 " + chain.length + " 个证书: ");
        MessageDigest sha1 = MessageDigest.getInstance("SHA1");
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        for (int i = 0; i < chain.length; i++) {
            X509Certificate cert = chain[i];
            System.out.println(" " + (i + 1) + " Subject " + cert.getSubjectDN());
            System.out.println("   Issuer  " + cert.getIssuerDN());
            sha1.update(cert.getEncoded());
            System.out.println("   sha1    " + toHexString(sha1.digest()));
            md5.update(cert.getEncoded());
            System.out.println("   md5     " + toHexString(md5.digest()));
            System.out.println();
        }

        X509Certificate cert = chain[1];
        String alias = host;
        customKs.setCertificateEntry(alias, cert);

        //如果文件不存在则创建文件
        if (!file.exists()) {
            if (file.createNewFile()) {
                System.out.println("创建 jssecacerts 秘钥库成功");
            }
        }
        OutputStream os = Files.newOutputStream(file.toPath());
        customKs.store(os, passphrases);
        os.close();

        System.out.println();
        System.out.println("已使用别名 " + alias + " 将证书添加到秘钥存储库 jssecacerts 中");
        System.out.println("jssecacerts 路径: " + path);
        System.out.println("可使用: keytool -list -keystore jssecacerts -storepass changeit 命令查看秘钥库信息");
    }

    private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();

    private static String toHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 3);
        for (int b : bytes) {
            b &= 0xff;
            sb.append(HEX_DIGITS[b >> 4]);
            sb.append(HEX_DIGITS[b & 15]);
            sb.append(' ');
        }
        return sb.toString();
    }

    private static class SavingTrustManager implements X509TrustManager {

        private final X509TrustManager tm;
        private X509Certificate[] chain;

        SavingTrustManager(X509TrustManager tm) {
            this.tm = tm;
        }

        public X509Certificate[] getAcceptedIssuers() {
            return chain;
        }

        public void checkClientTrusted(X509Certificate[] chain, String authType) {
            throw new UnsupportedOperationException();
        }

        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            this.chain = chain;
            tm.checkServerTrusted(chain, authType);
        }
    }

}

3.问题总结

运行上述代码之后,我们从下面的执行日志可以看出,当我们与目标服务器进行 SSL 握手失败后,就自动开始安装安全证书

加载秘钥存储库数据
打开链接 www.mca.gov.cn:443
开始SSL握手......

SSL握手失败: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

服务器发送了 3 个证书: 
 1 Subject CN=*.mca.gov.cn, O=民政部信息中心, L=北京, ST=北京, C=CN
   Issuer  CN=CFCA OV OCA, O=China Financial Certification Authority, C=CN
   sha1    5b 35 9f 3e 07 54 37 fd 49 87 c2 dc 80 60 4e 17 0c bb 4b 89 
   md5     21 d4 a4 28 eb d0 e9 e2 58 35 13 97 bf 54 f2 2f 

 2 Subject CN=CFCA OV OCA, O=China Financial Certification Authority, C=CN
   Issuer  CN=CFCA EV ROOT, O=China Financial Certification Authority, C=CN
   sha1    46 b0 ae c9 33 a6 26 f6 73 ba fb 74 41 c9 58 69 ea 94 31 46 
   md5     fe 5a 83 60 40 d6 5c 90 df 81 31 b6 7f 3c f9 5f 

 3 Subject CN=CFCA EV ROOT, O=China Financial Certification Authority, C=CN
   Issuer  CN=CFCA EV ROOT, O=China Financial Certification Authority, C=CN
   sha1    e2 b8 29 4b 55 84 ab 6b 58 c2 90 46 6c ac 3f b8 39 8f 84 83 
   md5     74 e1 b6 ed 26 7a 7a 44 30 33 94 ab 7b 27 81 30 

创建 jssecacerts 秘钥库成功

已使用别名 www.mca.gov.cn 将证书添加到秘钥存储库 jssecacerts 中
jssecacerts 路径: D:\Programming\Java\jdk1.8.0_351\jre\lib\security
可使用: keytool -list -keystore jssecacerts -storepass changeit 命令查看秘钥库信息

安装结束后,我们再执行一次代码可以看到,当SSL握手成功时,便不会再执行安装操作

加载秘钥存储库数据
打开链接 www.mca.gov.cn:443
开始SSL握手......
没有错误,证书已受信任

同时我们在 jssecacerts 目录下执行查看秘钥库信息的命令,便可以看到安全证书已经安装成功了

PS D:\Programming\Java\jdk1.8.0_351\jre\lib\security> keytool -list -keystore jssecacerts -storepass changeit
密钥库类型: JKS
密钥库提供方: SUN

您的密钥库包含 1 个条目

www.mca.gov.cn, 2024-3-13, trustedCertEntry,
证书指纹 (SHA-256): F0:7B:BB:DE:07:6F:9B:40:C5:7C:C4:BE:FE:DE:97:CA:1F:53:B9:AE:14:7F:03:5D:28:4C:BF:53:F3:43:2F:B8

这时候我们再去执行爬虫代码,也能正常获取网页数据

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李晋江

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值