错误信息
PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
也可能输出为
- 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.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
- unable to find valid certification path to requested target
JDK 21 和 25 会在前面加上 TLS 告警:“javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: …”。较旧 JDK 的日志显示为“javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: …”。框架和构建工具(Spring、Maven、Gradle、IDE)会把它包装进自己的消息中,但在异常原因(cause)中保留 PKIX 文本。
Java (JSSE, SunX509 trust manager) — javax.net.ssl.SSLHandshakeException caused by sun.security.validator.ValidatorException
含义
Java 的 PKIX 证书路径构建器无法从服务器证书出发,构建出一条通往 JVM 当前所用信任库中任何一个信任锚的链。“Requested target”指的就是服务器证书。Java 只信任它自己的 cacerts 文件(或用 javax.net.ssl.trustStore 指定的信任库),而不信任操作系统的信任库,因此由公司 CA 签发的证书、TLS 检查代理出示的证书,或者来自未发送中间证书的服务器的证书,验证都会在这里失败。
原因
服务器证书来自私有 CA
常见使用公司证书的内部服务、Kubernetes Ingress、Nexus 或 Artifactory 仓库以及数据库,其证书链指向的根证书不在 JDK 的 cacerts 中。
TLS 检查代理对连接重新签名
常见在公司网络中,Maven Central、Gradle 插件门户和公共 API 出示的都是由代理根证书签发的证书。把这个根证书安装到 Windows 或 macOS 中,对 Java 不起作用。
服务器没有发送中间证书
常见Java 默认不会获取缺失的中间证书(com.sun.security.enableAIAcaIssuers 处于关闭状态),因此只配置了叶子证书的服务器即使能被浏览器接受,Java 连接时也会失败。
JVM 使用的信任库和你以为的不是同一个
偶见IDE 和构建工具自带 JDK,应用服务器把 javax.net.ssl.trustStore 设置为只包含少数几个 CA 的信任库,容器则可能使用从未更新过 cacerts 的 JRE。导入到某个 JDK 的 cacerts 中的根证书,对另一个 JDK 不起作用。
过旧的 JDK 缺少较新的公共根证书
罕见在某个 CA 的根证书加入 cacerts 之前发布、且未打补丁的 Java 8 构建版本,访问使用该根证书的站点时会失败;当前的更新版本已包含这个根证书。
修复方法
- 1.
查看证书链和正在使用的信任库
openssl s_client 会显示服务器发送了什么。javax.net.debug=ssl:handshake:verbose 会让 Java 打印它加载的信任库路径,以及收到的每一张证书。
bash openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null 2>/dev/null \ | grep -E '^ *[0-9]+ s:|^ *i:' # Or from Java itself: prints the chain and the trust store in use java -Djavax.net.debug=ssl:handshake:verbose -jar app.jar 2>&1 | grep -E 'trustStore is|Subject:|Issuer:' - 2.
使用包含你的根证书的应用信任库
复制 JDK 的 cacerts(这样公共 CA 仍然可用),用 keytool 导入公司根证书或代理根证书,然后通过 javax.net.ssl.trustStore 和 trustStorePassword 让应用指向它(对于不是由你直接启动的工具,使用 JAVA_TOOL_OPTIONS)。
bash # A copy of the JDK's cacerts plus your root: public sites keep working 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 - 3.
把根证书导入 JDK 的 cacerts
keytool -cacerts 操作的是运行 keytool 的那个 JDK 的默认信任库(默认密码为“changeit”)。该 JDK 上的所有应用都会信任这个根证书;更换 JDK 后,这项修改就会丢失。
bash # Add the root to this JDK's default trust store (applies to every app on this JDK; # repeat after each JDK upgrade) sudo keytool -importcert -noprompt -cacerts -alias corp-root -file corp-root-ca.pem -storepass changeit - 4.
容器:让镜像导入你的 CA
设置了 USE_SYSTEM_CA_CERTS 时,Eclipse Temurin 镜像的入口点会在启动时把挂载在 /certificates 下的 *.crt 证书文件添加到 JVM 的 cacerts 中(只有使用镜像默认入口点时才会执行)。其他镜像需要在 Dockerfile 中加入 keytool 步骤。
bash # Eclipse Temurin images: mount your CA(s) as *.crt and let the entrypoint add them to cacerts docker run -e USE_SYSTEM_CA_CERTS=1 \ -v "$PWD/certs:/certificates/" eclipse-temurin:25.0.4.1_1-jdk-noble java -jar /app/app.jar - 5.
为单个客户端配置独立的信任库
先用你的信任库初始化 TrustManagerFactory,再用它构建 SSLContext,然后传给 HttpClient(或 OkHttp、Apache HttpClient、Netty)。切勿安装接受所有证书的信任管理器。
java // One client with its own trust store (verification stays on) 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();
相关错误
PKIX path validation failed: java.security.cert.CertPathValidatorException: val…Java (JSSE)java.security.InvalidAlgorithmParameterException: the trustAnchors parameter mu…Java (JSSE / PKIX)No subject alternative DNS name matching api.example.com found.Java (JSSE HostnameChecker)curl: (60) SSL certificate problem: unable to get local issuer certificatecurl (OpenSSL backend)x509: certificate signed by unknown authorityGo crypto/x509