跳到主要内容
ecdsa.com
← 全部文章

Java 的 PKIX path building failed:原因与真正的解决方法

你的浏览器信任什么,Java 并不跟着信任。它使用自己的信任库,默认不获取缺失的中间证书,而且无论哪种原因导致失败,报出的都是同一条消息。下面每一种原因都在 JDK 25 上复现过,并附有判断你遇到的是哪一种的命令。

简短回答:PKIX path building failed: ... unable to find valid certification path to requested target 表示 JVM 无法从服务器证书出发,构建出一条通往其实际加载的信任库中任何受信任根证书的链。这个信任库通常是 $JAVA_HOME/lib/security/cacerts,而不是操作系统的信任库。常见原因有:私有 CA 或代理的 CA 不在这个文件中;服务器没有发送中间证书;JVM 使用的信任库和你以为的不是同一个;JDK 太旧,缺少较新的根证书;缺少 SNI 名称。用 openssl s_client -showcerts 和 -Djavax.net.debug=ssl:handshake:verbose 查明是哪一种,然后修复信任库或服务器。切勿安装接受所有证书的信任管理器。

这条错误消息的含义

在当前的 JDK 上,完整的异常如下所示。JDK 21 和 25 会在前面加上 TLS 告警名称:

JDK 25.0.4.1,HttpClient 连接使用私有 CA 的服务器
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

逐项来看:

  • PKIX 是 RFC 5280 定义的 X.509 证书路径模型。Java 的实现位于 sun.security.provider.certpath。
  • 路径构建(path building)是两个步骤中的第一步。Java 会尝试找出任意一条从服务器的叶子证书向上通往信任锚的证书序列。只有这一步成功,路径验证才会检查日期、密钥用法、约束和吊销状态。第二步的失败有各自的错误消息,例如 PKIX path validation failed: validity check failed。
  • “requested target” 指的是服务器证书。

框架会包装这个异常,例如 Spring 的 ResourceAccessException、Maven 的“Could not transfer artifact”或 Gradle 的“Could not GET”。PKIX 文本仍然保留在异常原因链中,所以要在完整的堆栈跟踪里搜索它。

五种原因

这五种原因都用 eclipse-temurin:25.0.4.1_1-jdk-noble 和一个基于 java.net.http.HttpClient 的小程序复现过。测试 PKI 包含一张根证书、一张中间证书(“Issuing CA 1”)和一张 api.example.test 的叶子证书。

#情形结果
1私有 CA,默认的 JDK cacertsPKIX path building failed
2根证书已导入 cacerts 的副本,服务器发送叶子证书和中间证书HTTP 200
3同一信任库,服务器只发送叶子证书PKIX path building failed
4同第 3 行,加上 -Dcom.sun.security.enableAIAcaIssuers=truePKIX path building failed
4b同第 4 行,再加上 -Dcom.sun.security.allowedAIALocations=http://aia.example.test:8080/HTTP 200
5服务器按 SNI 选择证书,客户端发送 SNIHTTP 200
6同一服务器,-Djsse.enableSNIExtension=falsePKIX path building failed
7同一服务器,URL 使用 IP 地址(无 SNI)PKIX path building failed

1. 证书来自 Java 不信任的 CA

这是最常见的情况:使用公司证书的内部服务、Kubernetes Ingress、Nexus 或 Artifactory 以及数据库,还有经由 TLS 检查代理访问的每一个站点——这种代理会用自己的根证书对流量重新签名。把这个根证书安装到 Windows 或 macOS 中,浏览器就不再报错,但在大多数平台上 Java 并不读取操作系统的信任库。JDK 的 cacerts(在这个 Temurin 25 构建版本中有 118 个条目)根本不知道“Example Corp Root CA”是谁。

2. 服务器没有发送中间证书

服务器必须发送自己的叶子证书,以及直到根证书为止(不含根证书)的所有中间证书。许多服务器只配置了叶子证书。浏览器往往会掩盖这个错误,因为它们会缓存中间证书,或者从证书的颁发机构信息访问(Authority Information Access,AIA)扩展中的 URL 获取中间证书。Java 默认这两件事都不做。表中第 3 行和第 4 行就是这种情况。根证书是受信任的,但路径无法构建,因为 Java 根本看不到“Issuing CA 1”。

用两行命令就能看到服务器发送了什么:

openssl s_client -showcerts:8443 端口上的服务器发送证书链,8444 只发送叶子证书
$ openssl s_client -connect api.example.test:8443 -servername api.example.test -showcerts </dev/null 2>/dev/null | grep -E '^ *[0-9]+ s:|^ *i:'
 0 s:CN = api.example.test
   i:CN = Example Corp Issuing CA 1
 1 s:CN = Example Corp Issuing CA 1
   i:CN = Example Corp Root CA
$ openssl s_client -connect api.example.test:8444 -servername api.example.test -showcerts </dev/null 2>/dev/null | grep -E '^ *[0-9]+ s:|^ *i:'
 0 s:CN = api.example.test
   i:CN = Example Corp Issuing CA 1

如果最后一行 i: 给出的不是你信任的根证书,就说明缺少中间证书。证书链构建工具可以接收你粘贴的证书包,把证书排好顺序,通过 AIA 获取缺失的中间证书,并给出服务器应当发送的确切证书链。

3. JVM 使用的信任库和你以为的不是同一个

你已经导入了根证书,却仍然报错。常见原因如下:

  • 另一个 JDK:IDE、Gradle 工具链、Maven Wrapper、应用服务器和 Docker 镜像都自带 JDK。keytool -cacerts 修改的是运行 keytool 的那个 JDK 的信任库,而且只修改这一个。
  • 被替换的信任库:-Djavax.net.ssl.trustStore=only-corp.p12 会替换 cacerts,而不是在其基础上添加。只包含公司根证书的信任库会让所有公共站点都无法连接:
JDK 25:只包含公司根证书的信任库,连接 letsencrypt.org
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
  • 信任库路径拼写错误:如果 javax.net.ssl.trustStore 中的文件不存在,JDK 25 并不会报错。它会悄悄回退到 cacerts,于是你的根证书根本不在其中。只有信任管理器的调试输出会说明这一点:
JDK 25:-Djavax.net.ssl.trustStore=/tmp/app-trustore.p12(拼写错误),-Djavax.net.debug=ssl:trustmanager
Inaccessible trust store: /tmp/app-trustore.p12
trustStore is: /opt/java/openjdk/lib/security/cacerts
Fatal (CERTIFICATE_UNKNOWN): PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target (

如果信任库存在,但其中没有任何受信任证书,报出的错误就不同了:the trustAnchors parameter must be non-empty。

  • 框架级信任库:Spring Boot 的 SSL Bundle、Tomcat 连接器、Kafka 和数据库驱动都有自己的信任库设置,对于它们各自的连接,这些设置优先于 JVM 默认值。

JSSE 调试输出的第一行就能确定加载的是哪个文件(见诊断)。

4. JDK 太旧,缺少服务器所用的根证书

每个 JDK 更新版本都带有一份受信任根证书的快照。如果公共站点的证书链终止于一个较新的根证书,那么在早于该根证书的 JDK 上连接就会失败。Let's Encrypt 的兼容性页面给出了一个例子:ISRG Root X2 从 Java 8u401、11.0.22、17.0.10 和 21.0.2 起受信任。从 cacerts 的副本中删除两张根证书,就能看出过时的信任库会造成什么后果:

JDK 25,从 cacerts 副本中删除了 ISRG Root X1 和 X2
--- letsencrypt.org (chains to ISRG roots)
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
--- www.google.com (different CA)
HTTP 200

如果只有部分公共站点连接失败,先看它们的证书链通往哪个根证书,再看你的 JDK 是哪个更新版本。多年前从 cacerts 复制过来、之后再也没有更新过的信任库,也会出现同样的问题。

5. 没有 SNI,服务器发送了另一张证书

托管多个主机名的服务器会根据 ClientHello 中的服务器名称指示(SNI)选择证书。没有 SNI 时,它会发送默认证书,在测试中是一张自签名的 default.invalid 证书。这张证书无法追溯到任何根证书,所以报出的又是 PKIX 错误(第 6 行和第 7 行)。以下情况下 Java 不会发送 SNI:

  • URL 使用的是 IP 地址(TLS 禁止在 SNI 中使用 IP 字面量)
  • 设置了 -Djsse.enableSNIExtension=false,这往往是早年某个临时解决方案的遗留
  • 自定义的 SSLSocketFactory 在创建套接字时不带主机名

不带 -servername 的 openssl s_client 可以显示这类客户端收到的是什么。

逐步诊断

1. 查看服务器发送的证书链,尽可能在出错的那台机器上执行。代理可能会向那台机器出示另一张证书:

服务器发送了什么,每张证书由谁签名
openssl s_client -connect host.example:443 -servername host.example -showcerts </dev/null 2>/dev/null \
  | grep -E '^ *[0-9]+ s:|^ *i:'

如果经过 TLS 检查代理,最顶层的颁发者就是代理的 CA,而不是公共 CA。仅这一点就足以解释这个错误。

2. 让 Java 说明它加载了哪个信任库、收到了什么。调试输出很长,在我们的测试中,一次失败的握手就有大约 5,000 行,所以要过滤一下:

JSSE 调试输出,已过滤
java -Djavax.net.debug=ssl:handshake:verbose -jar app.jar 2>&1 \
  | grep -E 'trustStore is|Consuming server Certificate|"subject"|"issuer"|PKIX'
JSSE 调试输出中的关键行(JDK 25,私有 CA,节选)
trustStore is: /opt/java/openjdk/lib/security/cacerts
...
javax.net.ssl ... Consuming server Certificate handshake message (
"issuer"             : "CN=Example Corp Issuing CA 1",
"subject"            : "CN=api.example.test",
...
"issuer"             : "CN=Example Corp Root CA",
"subject"            : "CN=Example Corp Issuing CA 1",
...
javax.net.ssl ... Fatal (CERTIFICATE_UNKNOWN): PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target (

第一行给出了信任库文件。各个证书块就是服务器发送的证书链,从叶子证书开始排列;最后一个块的颁发者,就是 Java 没能连到任何根证书的那张证书。如果这个颁发者正是你本以为受信任的根证书或中间证书,就检查第一行给出的这个文件:

keytool:JVM 加载的信任库中有这张根证书吗?
$ keytool -list -cacerts -storepass changeit -alias corp-root
keytool error: java.lang.Exception: Alias <corp-root> does not exist

如果是其他文件,使用 keytool -list -keystore /path/to/store.p12。-v 会打印每个条目的主体和指纹,这样你就能按证书本身而不是按别名来匹配。

3. 要查看路径构建的详细信息,加上 -Djava.security.debug=certpath。它会记录构建器考虑过的每一个候选颁发者,以及在使用 AIA 时是否允许获取。

不关闭验证的修复方法

缺少中间证书时,先修复服务器。在 nginx、Apache、负载均衡器或 Ingress 中配置完整的证书链(先是叶子证书,然后是中间证书)。受益的是所有客户端,而不只是 Java。

如果 CA 是私有的,或者有代理对流量重新签名,就使用加入了你的根证书的应用信任库。从 cacerts 的副本开始,这样公共站点仍能正常访问:

应用信任库:JDK 自带的根证书加上公司根证书
cp "$JAVA_HOME/lib/security/cacerts" app-truststore.p12
keytool -importcert -noprompt -alias corp-root -file corp-root-ca.pem \
  -keystore app-truststore.p12 -storepass changeit
java -Djavax.net.ssl.trustStore=app-truststore.p12 -Djavax.net.ssl.trustStorePassword=changeit -jar app.jar

对于不是由你直接启动的工具,例如 Maven、Gradle 或由 IDE 运行的测试,通过 JAVA_TOOL_OPTIONS 或该工具自己的 JVM 选项传入相同的属性。keytool -importcert -cacerts 则会修改 JDK 的默认信任库。这会影响该 JDK 上的所有应用,但下次升级 JDK 时这项修改就会丢失。

如果只有一个集成需要额外的根证书,就为这一个客户端单独配置信任库。下面这个版本分别连接私有服务和一个公共站点实际运行过:

Java:为单个 HttpClient 配置 SSLContext
KeyStore ts = KeyStore.getInstance("PKCS12");
try (InputStream in = Files.newInputStream(Path.of("app-truststore.p12"))) {
    ts.load(in, "changeit".toCharArray());
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ts);
SSLContext ssl = SSLContext.getInstance("TLS");
ssl.init(null, tmf.getTrustManagers(), null);
HttpClient client = HttpClient.newBuilder().sslContext(ssl).build();
// https://api.example.test:8443/ -> HTTP 200
// https://letsencrypt.org/ -> HTTP 200

OkHttp、Apache HttpClient、Netty、gRPC 和 JDBC 驱动都接受用同样方式构建的 SSLContext 或 TrustManager。

在容器中,如果设置了 USE_SYSTEM_CA_CERTS,并且把 *.crt 文件挂载到 /certificates/,Eclipse Temurin 镜像会在启动时导入这些额外的 CA:

docker run -e USE_SYSTEM_CA_CERTS=1 -v ./certs:/certificates/ eclipse-temurin:25.0.4.1_1-jdk-noble(节选)
Adding certificate with alias Example Corp Root CA to the JVM truststore
Your keystore contains 240 entries

只有通过镜像的默认入口点启动时才会执行这一步。使用其他基础镜像时,在 Dockerfile 中运行 keytool -importcert。

在 macOS 上使用 JDK 23 或更高版本时,-Djavax.net.ssl.trustStoreType=KeychainStore-ROOT 会让 Java 使用系统的根证书钥匙串,这样 IT 部门安装的根证书就会受信任(JDK-8320362)。在 Windows 上,-Djavax.net.ssl.trustStoreType=Windows-ROOT 对 Windows 根证书库起同样的作用。

缺少公共根证书时,升级 JDK。在旧 JDK 上手动修补 cacerts,会让它错过其余的安全修复。

为什么“信任所有证书”不是修复方法

网上最流行的答案是实现一个 X509TrustManager,让它的 checkServerTrusted 什么也不做,往往还加上一个直接返回 true 的 HostnameVerifier。异常确实消失了,但代价是把 TLS 变成了一种根本不验证对端是谁的加密。任何能拦截连接的一方都可以出示任意证书,读取并篡改流量:恶意 Wi-Fi 网络、被攻破的路由器、配置错误的代理、DNS 劫持。这类“临时”代码还往往会一直留下来,并被复制到下一个项目中。上面的每一种修复方法都只需要几分钟,而且不会关闭验证。

把服务器自己的证书作为信任锚导入也是同样的道理。这种做法只在下一次续期之前有效,而随着证书有效期缩短,下一次续期就在几周之内。应该信任签发证书的 CA,而不是证书本身。

检查清单

  1. 在出错的主机上运行 openssl s_client -showcerts:证书链是否完整,最顶层的颁发者是谁?
  2. -Djavax.net.debug=ssl:handshake:verbose:trustStore is: 行显示 JVM 加载的是哪个文件?
  3. 对这个文件运行 keytool -list:签发该证书链的根证书是否在其中?
  4. 路径上是否有 TLS 检查代理?如果有,你需要的就是它的根证书。
  5. 是否发送了 SNI?检查 IP 字面量、jsse.enableSNIExtension 和自定义套接字工厂。
  6. JDK 是否足够新,已经包含该站点所用的根证书?

参考资料

  • RFC 5280:证书路径验证(§6)。
  • Oracle,JDK 21.0.10 版本说明:AIA 扩展中 URI 的过滤规则(com.sun.security.allowedAIALocations)。
  • JDK 25 自带的 java.security 文件($JAVA_HOME/conf/security/java.security)中的“X.509 AuthorityInfoAccess caIssuer URI Filtering”一节。
  • JDK-8320362:从钥匙串密钥库加载信任锚证书(KeychainStore-ROOT,JDK 23)。
  • Let's Encrypt,证书兼容性。
  • Eclipse Temurin 容器镜像:入口点选项 USE_SYSTEM_CA_CERTS。

本页的所有结果均于2026年10月20日在 eclipse-temurin:25.0.4.1_1-jdk-noble(OpenJDK 25.0.4.1,测试服务器使用 OpenSSL 3.0.13)中得出,测试证书也是在该容器中生成的。