当前位置:首页 > 技术 > 正文内容

解决Spring Boot应用中SSL握手失败与PKIX路径构建异常

访客 技术 2026年6月17日 1

问题现象

在本地开发或生产环境部署Spring Boot应用时,发起HTTPS请求可能会遇到以下SSL握手异常:

javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

原因分析

该异常的根本原因是Java运行环境(JRE/JDK)的默认信任库(通常为 cacerts)中缺少目标服务器SSL证书的根证书或中间证书。这在访问使用自签名证书、内部CA签发证书或证书链配置不完整的HTTPS站点时尤为常见。

解决方案:导入目标证书至JDK信任库

要彻底解决此问题,需要将目标服务器的证书提取出来,并导入到Java的信任库中。以下是具体的操作步骤。

1. 编译证书导入工具

创建一个名为 CertificateImporter.java 的文件,将以下重构后的工具类代码粘贴进去并编译。该工具会自动连接目标服务器、抓取证书链并生成新的信任库文件。

import javax.net.ssl.*;
import java.io.*;
import java.security.*;
import java.security.cert.*;
import java.util.Scanner;

public class CertificateImporter {

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

    public static void main(String[] args) throws Exception {
        if (args.length < 1 || args.length > 2) {
            System.out.println("用法: java CertificateImporter <主机名>[:<端口>] [密钥库密码]");
            return;
        }

        String[] hostAndPort = args[0].split(":");
        String targetHost = hostAndPort[0];
        int targetPort = hostAndPort.length > 1 ? Integer.parseInt(hostAndPort[1]) : 443;
        char[] storePassword = (args.length == 2 ? args[1] : "changeit").toCharArray();

        File keystoreFile = resolveKeystoreFile();
        System.out.println("正在加载密钥库: " + keystoreFile.getAbsolutePath());
        
        KeyStore keyStore = loadKeyStore(keystoreFile, storePassword);

        SSLContext sslContext = SSLContext.getInstance("TLS");
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(keyStore);
        
        X509TrustManager defaultTm = (X509TrustManager) tmf.getTrustManagers()[0];
        CertificateCapturingTrustManager capturingTm = new CertificateCapturingTrustManager(defaultTm);
        
        sslContext.init(null, new TrustManager[]{capturingTm}, null);
        SSLSocketFactory socketFactory = sslContext.getSocketFactory();

        System.out.println("正在连接 " + targetHost + ":" + targetPort + " ...");
        try (SSLSocket socket = (SSLSocket) socketFactory.createSocket(targetHost, targetPort)) {
            socket.setSoTimeout(10000);
            socket.startHandshake();
            System.out.println("握手成功,证书已受信任,无需额外操作。");
            return;
        } catch (SSLException e) {
            System.out.println("握手失败,准备提取服务器证书...");
        }

        X509Certificate[] serverChain = capturingTm.getCapturedChain();
        if (serverChain == null) {
            System.err.println("未能获取服务器证书链。");
            return;
        }

        printCertificateChain(serverChain);

        System.out.print("请输入要添加到信任库的证书编号 (或输入 'q' 退出) [1]: ");
        Scanner scanner = new Scanner(System.in);
        String input = scanner.nextLine().trim();
        
        if ("q".equalsIgnoreCase(input)) {
            System.out.println("已取消操作。");
            return;
        }

        int certIndex = input.isEmpty() ? 0 : Integer.parseInt(input) - 1;
        X509Certificate targetCert = serverChain[certIndex];
        String alias = targetHost + "-" + (certIndex + 1);
        
        keyStore.setCertificateEntry(alias, targetCert);

        File outputFile = new File("jssecacerts");
        try (OutputStream out = new FileOutputStream(outputFile)) {
            keyStore.store(out, storePassword);
        }
        
        System.out.println("成功将证书添加到 '" + outputFile.getAbsolutePath() + "',别名为 '" + alias + "'。");
    }

    private static File resolveKeystoreFile() {
        String javaHome = System.getProperty("java.home");
        File jssecacerts = new File("jssecacerts");
        if (jssecacerts.isFile()) return jssecacerts;

        File securityDir = new File(javaHome, "lib/security");
        jssecacerts = new File(securityDir, "jssecacerts");
        if (jssecacerts.isFile()) return jssecacerts;

        return new File(securityDir, "cacerts");
    }

    private static KeyStore loadKeyStore(File file, char[] password) throws Exception {
        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
        try (InputStream in = new FileInputStream(file)) {
            ks.load(in, password);
        }
        return ks;
    }

    private static void printCertificateChain(X509Certificate[] chain) throws Exception {
        System.out.println("\n服务器返回了 " + chain.length + " 个证书:\n");
        MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
        MessageDigest md5 = MessageDigest.getInstance("MD5");

        for (int i = 0; i < chain.length; i++) {
            X509Certificate cert = chain[i];
            System.out.println(" " + (i + 1) + " 主题: " + cert.getSubjectX500Principal());
            System.out.println("   颁发者: " + cert.getIssuerX500Principal());
            
            sha1.update(cert.getEncoded());
            System.out.println("   SHA1: " + toHex(sha1.digest()));
            
            md5.update(cert.getEncoded());
            System.out.println("   MD5:  " + toHex(md5.digest()));
            System.out.println();
        }
    }

    private static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 3);
        for (byte b : bytes) {
            int v = b & 0xFF;
            sb.append(HEX_CHARS[v >>> 4]);
            sb.append(HEX_CHARS[v & 0x0F]);
            sb.append(' ');
        }
        return sb.toString().trim();
    }

    private static class CertificateCapturingTrustManager implements X509TrustManager {
        private final X509TrustManager delegate;
        private X509Certificate[] capturedChain;

        public CertificateCapturingTrustManager(X509TrustManager delegate) {
            this.delegate = delegate;
        }

        public X509Certificate[] getCapturedChain() {
            return capturedChain;
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return delegate.getAcceptedIssuers();
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            delegate.checkClientTrusted(chain, authType);
        }

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

在终端中执行编译命令:

javac CertificateImporter.java

2. 运行工具抓取证书

使用编译后的类文件运行工具,传入目标域名或IP(可带端口)。例如,访问 kyfw.12306.cn

java CertificateImporter kyfw.12306.cn

程序会打印出服务器返回的证书链信息,并提示输入要信任的证书编号。通常情况下,输入 1 并回车即可将根证书或服务器证书加入信任库。

请输入要添加到信任库的证书编号 (或输入 'q' 退出) [1]: 1

3. 替换JDK默认信任库

执行成功后,当前目录下会生成一个名为 jssecacerts 的文件。接下来,需要将该文件复制到Java环境的安全目录中:

  • JDK 8 及以下版本:复制到 $JAVA_HOME/jre/lib/security/ 目录下。
  • JDK 9 及以上版本:复制到 $JAVA_HOME/lib/security/ 目录下。

注意:如果目标目录已存在 jssecacerts 文件,建议先备份原文件再进行覆盖。Java在启动时会优先加载 jssecacerts,若不存在则回退加载 cacerts

4. 重启应用

完成信任库文件的替换后,重启Spring Boot项目。此时JVM将能够识别并信任目标服务器的SSL证书,握手异常即可消除。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。