解决javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed 错误?

612

编辑:我在我的博客上尝试以更加整洁的方式格式化了问题和已接受答案。

以下是原始问题。

我遇到了这个错误:

详细信息 sun.security.validator.ValidatorException: PKIX路径构建失败:
sun.security.provider.certpath.SunCertPathBuilderException: 无法找到到请求目标的有效认证路径

原因 javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX路径构建失败: sun.security.provider.certpath.SunCertPathBuilderException: 无法找到到请求目标的有效认证路径

我正在使用 Tomcat 6 作为 web 服务器。在同一台机器上,我安装了两个 HTTPS Web 应用程序,它们分别位于不同的 Tomcat 上,使用不同的端口。假设 App1 (端口 8443)连接到 App2 (端口 443)。当 App1 连接到 App2 时,我会收到上述错误。我知道这是一个非常常见的错误,因此在不同的论坛和站点上找到了许多解决方案。我在两个 Tomcats 的 server.xml 中都有以下条目:

keystoreFile="c:/.keystore" 
keystorePass="changeit"

每个网站都说同样的原因,即由app2提供的证书不在app1 jvm的受信任存储中。当我尝试在IE浏览器中访问相同的URL时,它可以正常工作(需要警告:“这个网站的安全证书存在问题。在此我选择继续访问此网站)。但是当Java客户端尝试访问相同的URL时(在我的情况下),我会收到上述错误。因此,为了将其放入受信任的存储中,我尝试了以下三个选项:

选项1

System.setProperty("javax.net.ssl.trustStore", "C:/.keystore");
System.setProperty("javax.net.ssl.trustStorePassword", "changeit");

选项2

在环境变量中设置如下内容

CATALINA_OPTS -- param name
-Djavax.net.ssl.trustStore=C:\.keystore -Djavax.net.ssl.trustStorePassword=changeit ---param value

选项 3

在环境变量中设置以下内容。

JAVA_OPTS -- param name
-Djavax.net.ssl.trustStore=C:\.keystore -Djavax.net.ssl.trustStorePassword=changeit ---param value

结果

但是没有任何一种方法起作用。

最后起作用的方法是执行Pascal Thivent在如何处理Apache HttpClient中无效的SSL证书?中建议的Java方法,即执行InstallCert程序。

但是这种方法适用于开发环境设置,而我不能在生产环境中使用它。

我想知道为什么在App1中将相同的值在App2服务器的server.xml中和信任存储中提到了三种上述方法都不起作用,同时在App1程序中通过以下方式设置:

System.setProperty("javax.net.ssl.trustStore", "C:/.keystore") and System.setProperty("javax.net.ssl.trustStorePassword", "changeit");

有关更多信息,以下是我建立连接的方法:

URL url = new URL(urlStr);

URLConnection conn = url.openConnection();

if (conn instanceof HttpsURLConnection) {

  HttpsURLConnection conn1 = (HttpsURLConnection) url.openConnection();
  
  conn1.setHostnameVerifier(new HostnameVerifier() {
    public boolean verify(String hostname, SSLSession session) {
      return true;
    }
  });

  reply.load(conn1.getInputStream());

可能是 HttpClient 和 SSL 的重复问题。 - user207421
有趣的是,我在通信两个没有SSL问题的集群服务器之间时遇到了这个错误。一旦我在我的RHEL服务器上正确设置了“domainname”,问题就解决了。希望能对某些人有所帮助。 - DavidGamba
另外要检查的一件事是你是否拥有最新版本的Java - 我之前遇到类似的错误就是因为这个原因。 - Redzarf
https://dev59.com/s3E85IYBdhLWcg3wMQlm - 这也是相关的,提供了非常好的答案。 - Siddhartha
首先将您的crt文件导入{JAVA_HOME}/jre/security/cacerts,如果您仍然面对此异常,请更改您的jdk版本。例如从jdk1.8.0_17更改为jdk1.8.0_231。 - Tohid Makari
我在Windows上遇到了这个问题,我在这里找到了答案:https://dev59.com/ulgR5IYBdhLWcg3wf9VZ - David Marciel
38个回答

536

你需要将App2的证书添加到使用的JVM的信任存储文件中,该文件位于$JAVA_HOME\lib\security\cacerts

首先,您可以通过运行以下命令检查您的证书是否已经存在于信任存储中:keytool -list -keystore "$JAVA_HOME/jre/lib/security/cacerts"(无需提供密码)

如果缺少您的证书,您可以使用浏览器下载它并使用以下命令将其添加到信任存储中:

keytool -import -noprompt -trustcacerts -alias <AliasName> -file   <certificate> -keystore <KeystoreFile> -storepass <Password>

示例:

keytool -import -noprompt -trustcacerts -alias myFancyAlias -file /path/to/my/cert/myCert.cer -keystore /path/to/my/jdk/jre/lib/security/cacerts/keystore.jks -storepass changeit

导入后,您可以再次运行第一个命令以检查证书是否已添加。

有关Sun/Oracle信息,请在此处查找。


9
你需要使用完整路径,例如 c:\java\jdk\lib\security\cacerts。 - SimonSez
66
如SimonSez所说,您不需要密码,但如果您想要的话,默认密码是“changeit”。 - Felix
25
此外,在Windows中,您需要以管理员身份运行终端,否则在尝试导入证书时会出现错误“keytool error: java.io.FileNotFoundException ...(拒绝访问)”。 - Felix
2
啊,@SimonSez,你是我的救星。但是要补充一点,必须像@M Sach提到的那样指定信任存储位置和密码才能使其正常工作。 - BudsNanKis
2
继续遇到Java 1.8的问题。需要按照说明添加证书并使用Java < 1.8。 - Tom Howard
显示剩余20条评论

241

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

这个错误通常发生在服务器更改了 HTTPS SSL 证书,而旧版本的 Java 不认识根证书颁发机构(CA)。如果你能够在浏览器中访问该 HTTPS URL,则可以更新 Java 版本以识别根 CA。

在浏览器中打开无法访问的 HTTPS URL,点击 HTTPS 证书链(Internet Explorer 中有锁图标),然后点击锁定以查看证书。进入证书的“详细信息”并选择“复制到文件”,将其以Base64 (.cer)格式复制到桌面上。

安装证书时忽略所有警告。这是我获取要访问的 URL 的证书信息的方法。

接下来,我需要让我的 Java 版本知道这个证书,以免它拒绝识别该 URL。在此方面,我必须提到默认情况下 JDK 的 \jre\lib\security 位置存储着根证书信息,用于访问的默认密码为:changeit。

以下是查看 cacerts 信息的步骤:

• 单击“开始”按钮-->运行

• 输入 cmd。打开命令提示符(您可能需要以管理员身份打开它)。

• 进入您的 Java/jreX/bin 目录

• 输入以下内容:

keytool -list -keystore D:\Java\jdk1.5.0_12\jre\lib\security\cacerts

它提供了当前包含在密钥库中的证书列表。它看起来像这样:

C:\Documents and Settings\NeelanjanaG>keytool -list -keystore D:\Java\jdk1.5.0_12\jre\lib\security\cacerts

Enter keystore password:  changeit

Keystore type: jks

Keystore provider: SUN

Your keystore contains 44 entries

verisignclass3g2ca, Mar 26, 2004, trustedCertEntry,

Certificate fingerprint (MD5): A2:33:9B:4C:74:78:73:D4:6C:E7:C1:F3:8D:CB:5C:E9

entrustclientca, Jan 9, 2003, trustedCertEntry,

Certificate fingerprint (MD5): 0C:41:2F:13:5B:A0:54:F5:96:66:2D:7E:CD:0E:03:F4

thawtepersonalbasicca, Feb 13, 1999, trustedCertEntry,

Certificate fingerprint (MD5): E6:0B:D2:C9:CA:2D:88:DB:1A:71:0E:4B:78:EB:02:41

addtrustclass1ca, May 1, 2006, trustedCertEntry,

Certificate fingerprint (MD5): 1E:42:95:02:33:92:6B:B9:5F:C0:7F:DA:D6:B2:4B:FC

verisignclass2g3ca, Mar 26, 2004, trustedCertEntry,

Certificate fingerprint (MD5): F8:BE:C4:63:22:C9:A8:46:74:8B:B8:1D:1E:4A:2B:F6

• 现在我需要将之前安装的证书包含在cacerts中。

• 操作步骤如下:

keytool -import -noprompt -trustcacerts -alias ALIASNAME -file FILENAME_OF_THE_INSTALLED_CERTIFICATE -keystore PATH_TO_CACERTS_FILE -storepass PASSWORD

如果你正在使用Java 7:

keytool -importcert -trustcacerts -alias ALIASNAME -file PATH_TO_FILENAME_OF_THE_INSTALLED_CERTIFICATE -keystore PATH_TO_CACERTS_FILE -storepass changeit

• 然后它将把证书信息添加到cacert文件中。

这是我为上面提到的异常找到的解决方案!


8
证书过期后应该怎么办?需要每年重复所有流程吗? - ggkmath
8
有没有一种以编程方式实现这个的方法? - moomoohk
3
对于遇到PKIX错误、“路径不与任何信任锚匹配”的人来说,不幸的是这个解决方案并没有解决我的问题。 - IcedDante
5
一个问题 - aliasName是我们导入证书的网址吗?例如,如果URL是http://domain.site.com/pages/service.asmx,那么alias应该是domain.site.com还是完整的URL(domain.site.com/pages/service.asmx),或者它也应该加上http://前缀,还是只是任意名称? 答案:aliasName应该是网址的主机名部分,即“domain.site.com”,不需要包括完整的URL或http://前缀。 - nanosoft
2
路径:\lib\security> keytool -import -noprompt -trustcacerts -alias webCert -file webCertResource.cer -keystore c:/Users/Jackie/Desktop -storepass changeit。我收到了“系统找不到指定的文件”的错误提示。 - JayC
显示剩余9条评论

63

如何在Tomcat 7中使用它

我想在Tomcat应用程序中支持自签名证书,但以下代码片段未能正常工作

import java.io.DataOutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class HTTPSPlayground {
    public static void main(String[] args) throws Exception {

        URL url = new URL("https:// ... .com");
        HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();

        httpURLConnection.setRequestMethod("POST");
        httpURLConnection.setRequestProperty("Accept-Language", "en-US,en;q=0.5");
        httpURLConnection.setDoOutput(true);
        DataOutputStream wr = new DataOutputStream(httpURLConnection.getOutputStream());

        String serializedMessage = "{}";
        wr.writeBytes(serializedMessage);
        wr.flush();
        wr.close();

        int responseCode = httpURLConnection.getResponseCode();
        System.out.println(responseCode);
    }
}

以下是解决我的问题的方法:

1)下载 .crt 文件

echo -n | openssl s_client -connect <your domain>:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > ~/<your domain>.crt
  • <your domain>替换为您的域名(例如:jossef.com

2)在Java的cacerts证书存储中应用.crt文件

keytool -import -v -trustcacerts -alias <your domain> -file ~/<your domain>.crt -keystore <JAVA HOME>/jre/lib/security/cacerts -keypass changeit -storepass changeit
  • 用您的域名替换<your domain>(例如:jossef.com
  • 用您的Java主目录替换<JAVA HOME>

3) 突破它

尽管我已经将证书安装在Java的默认证书存储中,但是 Tomcat忽视了这一点 (似乎没有配置为使用Java的默认证书存储)。

要进行突破,请在代码的某个位置添加以下内容:

String certificatesTrustStorePath = "<JAVA HOME>/jre/lib/security/cacerts";
System.setProperty("javax.net.ssl.trustStore", certificatesTrustStorePath);

// ...

5
使用SpringBoot和Tomcat 7,第二步对我很有帮助。谢谢。 - Tim Perry
我在导入“.crt”时遇到了“密钥库被篡改或密码不正确”的错误提示。 - prayagupa
@prayagupd - 也许存储密码不同?默认密码是 changeit。请参见 https://dev59.com/GWQn5IYBdhLWcg3wXGKr - Jossef Harush Kadouri
1
这个有效!非常感谢@JossefHarush提供如此有用的答案! - Tom Taylor
1
在我的代码中添加@Jossef Harush的代码段后,我的问题得到了解决。 - Chamod Pathirana
显示剩余4条评论

24
在我的情况下,问题是Web服务器只发送了证书和中间CA,而没有根CA。添加以下JVM选项可以解决该问题:-Dcom.sun.security.enableAIAcaIssuers=true

支持Authority Information Access扩展的caIssuers访问方法可用。出于兼容性考虑,默认情况下禁用,可以通过将系统属性 com.sun.security.enableAIAcaIssuers设置为true来启用。

如果设置为true,则CertPathBuilder的Sun PKIX实现将使用证书的AIA扩展中的信息(除了指定的CertStores)来查找颁发CA证书,前提是它是ldap、http或ftp类型的URI。

来源

12

可以通过编程方式禁用SSL验证。对于开发人员可能会有帮助,但不建议在生产环境中使用,因为生产环境中需要使用“真正”的SSL验证或安装并使用自己的受信任密钥,然后仍然要使用“真正”的SSL验证。

以下代码适用于我:

import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.X509TrustManager;

public 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 null;
  }
}

             HttpsURLConnection conn = null;
             URL url = new URL(serviceUrl);
             conn = (HttpsURLConnection) url.openConnection();
             SSLContext sc = SSLContext.getInstance("SSL");  
             sc.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new java.security.SecureRandom());  
                    // Create all-trusting host name verifier
             HostnameVerifier allHostsValid = new HostnameVerifier() {
               public boolean verify(String hostname, SSLSession session) {
                return true;
              }
            };
            conn.setHostnameVerifier(allHostsValid);

如果你无法控制底层的Connection,你也可以为所有连接全局覆盖SSL验证 https://dev59.com/8GIk5IYBdhLWcg3wJ7L4#19542614

如果你使用的是Apache HTTPClient,则必须以“不同”的方式禁用它(遗憾的是):https://dev59.com/6nE85IYBdhLWcg3wl0nF#2703233


20
这段代码完全不安全,不应使用。 - user207421
1
@user207421 为什么它不安全?简要说明一下代码中发生了什么。 - Govinda Sakhare
3
这样做是跳过了所有证书验证,基本上是允许接受任何证书。证书的工作方式是有一个根证书(字面上)在各个认证机构中受到物理保护。然后使用该证书发行其他次级证书,可以一直验证回根认证机构。这将跳过所有上游检查,意味着我可以发送任何ssl证书(甚至自己生成的),而您的应用程序将其接受为安全的,尽管我的身份作为url未经验证。 - Scott Taylor
感谢您的解决方案。您缺少了一行代码,实际上使用了sc这个未使用的变量,因此是无用的: conn.setSSLSocketFactory(sc.getSocketFactory()); - undefined

11

另一个可能的原因是JDK版本过旧。我使用的是jdk版本1.8.0_60,仅通过更新到最新版本就解决了证书问题。


3
我也遇到了同样的问题。 使用Let's Encrypt证书调用API可能无法在旧版本的Java中工作,因为它未被受信任的根证书颁发机构所识别。 更新Java将解决此问题。 - hertg

7

我在使用 jdk1.8.0_171 时遇到了同样的问题。我尝试了这里提供的前两种解决方案(使用 keytool 添加证书和另一种含有 hack 的解决方案),但它们对我都不起作用。

后来我将 JDK 升级到了 1.8.0_181,问题迎刃而解。


5

我的cacerts文件完全为空。我解决了这个问题,方法是从我的Windows机器上复制cacerts文件(使用的是Oracle Java 7),然后将其scp到我的Linux机器上(使用OpenJDK)。

cd %JAVA_HOME%/jre/lib/security/
scp cacerts mylinuxmachin:/tmp

接着在 Linux 机器上执行以下操作

cp /tmp/cacerts /etc/ssl/certs/java/cacerts

到目前为止,它一直表现得非常出色。


1
如果问题是您使用的Java版本较旧,没有最新的证书,那么这个方法非常有效。 - atripathi
@atripathi 你觉得 Mac 怎么样? - iOSAndroidWindowsMobileAppsDev
如果cacerts文件为空,则您的Java安装存在严重问题。您应该重新安装它。 - user207421
也许吧,但这个解决方案有效,并且之后再也没有出现过问题。 - Ryan Shillington

5

可部署解决方案(Alpine Linux)

为了能够解决我们应用环境中的这个问题,我们已经准备好以下Linux终端命令:

cd ~

将在主目录中生成证书文件。

apk add openssl

这条命令安装了openssl在alpine Linux中。你可以为其他Linux分发找到适当的命令。
openssl s_client -connect <host-dns-ssl-belongs> < /dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > public.crt

生成所需的证书文件。

sudo $JAVA_HOME/bin/keytool -import -alias server_name -keystore $JAVA_HOME/lib/security/cacerts -file public.crt -storepass changeit -noprompt

使用程序“keytool”将生成的文件应用于JRE。

注意:请将您的DNS替换为<host-dns-ssl-belongs>

注意2:请注意,-noprompt不会提示验证消息(是/否),-storepass changeit参数将禁用密码提示并提供所需的密码(默认为“changeit”)。这两个属性将让您在应用环境中使用这些脚本,例如构建Docker镜像。

注意3:如果您通过Docker部署应用程序,则可以生成一次秘密文件,并将其放入您的应用程序项目文件中。您不需要再次生成它。


5

在Linux下使用Tomcat 7,这就是解决方法。

String certificatesTrustStorePath = "/etc/alternatives/jre/lib/security/cacerts";
System.setProperty("javax.net.ssl.trustStore", certificatesTrustStorePath);
System.setProperty("javax.net.ssl.trustStorePassword", "changeit");

在Linux下,$JAVA_HOME不一定被设置,但通常/etc/alternatives/jre指向$JAVA_HOME/jre

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接