文章目录

前言报错背景1、禁用SSL证书验证2、证书添加到Java信任库3、手动创建信任库-添加证书 - 推荐4、JVM参数设置信任库

踩坑maven打包后报错Invalid keystore format

前言

在Java中进行网络请求时出现"sun.security.validator.ValidatorException: PKIX path building failed"错误通常是由于SSL证书验证失败引起的。这种错误可能由以下几种原因导致:

1、证书链不完整或证书不受信任: Java使用TrustStore来验证SSL证书的有效性。如果服务器使用的SSL证书不在Java TrustStore中,或者证书链不完整,就会导致PKIX路径构建失败。

2、证书过期: 如果服务器的SSL证书已经过期,Java会拒绝建立与该服务器的安全连接,从而导致PKIX路径构建失败。

3、证书主题名称与服务器域名不匹配: SSL证书通常与特定的域名相关联。如果SSL证书的主题名称与服务器的域名不匹配,Java会认为连接不安全而拒绝连接,从而导致PKIX路径构建失败。

下面我提供了两种解决方案:

1、禁用SSL证书验证 2、证书添加Java信任库 3、手动创建信任库 - 推荐 4、JVM设置信任库

建议使用第三种方式,灵活,证书过期可以不用重新部署应用。

报错背景

代码如下就几行

String requestUrl = "https://subconverter.hladder.xyz/version";

RestTemplate restTemplate = new RestTemplate();

ResponseEntity response = restTemplate.exchange(requestUrl, HttpMethod.GET, null, String.class);

String responseBody = response.getBody();

System.out.println(responseBody);

为了确保这个url是可用的,在浏览器测试一下。可以看到是ok的。 接下来运行上面的java代码可以看到直接就报错了。为什么会出现下面的报错?因为Java对SSL证书的信任链有严格的要求。即使URL在浏览器中可访问,但如果SSL证书不在Java的信任库中,Java程序仍然可能会出现证书验证错误,导致无法建立安全连接。 下面是提供了如何解决这个问题的方案。

1、禁用SSL证书验证

代码如下

// 创建一个RestTemplate实例

RestTemplate restTemplate = new RestTemplate();

// 要请求的URL

String requestUrl = "https://subconverter.hladder.xyz/version";

// 禁用SSL证书验证

TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {

@Override

public java.security.cert.X509Certificate[] getAcceptedIssuers() {

return null;

}

@Override

public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {

}

@Override

public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {

}

}};

// 创建SSLContext,使用禁用SSL证书验证的TrustManager

SSLContext sslContext = SSLContext.getInstance("SSL");

sslContext.init(null, trustAllCerts, new java.security.SecureRandom());

// 设置全局默认的SSLSocketFactory,使RestTemplate使用禁用SSL证书验证的SSLContext

HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());

// 发送HTTP GET请求并接收响应

ResponseEntity response = restTemplate.exchange(requestUrl, HttpMethod.GET, null, String.class);

// 获取响应体

String responseBody = response.getBody();

// 输出响应体内容

System.out.println(responseBody);

运行代码之后成功获取到了结果,并且没有报错。

2、证书添加到Java信任库

1、首先得下载服务器的SSL证书公钥文件。

我的服务器使用的是caddy做反向代理的,并且是用docker部署的,并且已经把caddy容器的/data目录映射到主机的/data/caddy/data,所以很容易就能找到公钥文件。

文件位置在/data/caddy/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory下面。不会caddy的可以看下面的文章。Caddy 自动HTTPS 反向代理、重定向、静态页面 - docker版

下载.crt结尾的证书文件。我把它放到resources目录下。 下面我将把证书添加到信任库文件中。

1、使用管理员身份运行cmd,进入到resources目录

2、执行keytool指令

-file后面的文件名修改为自己的。同时修改jdk安装目录下的cacerts文件的位置,注意需要绝对路径才行。

keytool -import -alias subconverter -file "E:\idea项目\sifanERP\h3yun-api\src\main\resources\subconverter.hladder.xyz.crt" -keystore "C:\Program Files\Java\jdk1.8.0_40\jre\lib\security\cacerts" -storepass changeit

命令解释

这条命令的含义是将一个证书文件导入到 Java 的信任库中。

keytool: 是 Java 提供的一个用于管理密钥和证书的命令行工具。-import: 表示进行证书的导入操作。-alias subconverter: 设置导入的证书的别名为 “subconverter”,以后可以通过这个别名来识别和管理该证书。-file "E:\idea项目\sifanERP\h3yun-api\src\main\resources\subconverter.hladder.xyz.crt": 指定要导入的证书文件的路径。在这个例子中,证书文件位于指定的路径下。-keystore "C:\Program Files\Java\jdk1.8.0_40\jre\lib\security\cacerts": 指定信任库文件的路径。在这个例子中,信任库文件位于指定的路径下。-storepass changeit: 指定信任库的密码。在这个例子中,使用的是默认的信任库密码 “changeit”。

执行该命令后,系统会将指定的证书文件导入到 Java 的信任库中,并使用指定的别名存储。在这之后,您就可以在 Java 程序中使用该别名来引用这个证书。

jre中是有cacerts文件的。上面的治疗是往cacerts文件插入内容。cacerts这是个二进制文件。3、在控制台粘贴上面的指令,回车。 下面会让你手动输入是否信任此证书,输入是并回车。提示证书已添加到密钥库中并且没有任何报错才算成功。如果 有报错需要检查文件名和路径的问题。4、测试 测试代码如下:

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

TestNPTO testNPTO = new TestNPTO();

testNPTO.test02();

}

void test02() throws Exception {

String requestUrl = "https://subconverter.hladder.xyz/version";

RestTemplate restTemplate = new RestTemplate();

ResponseEntity response = restTemplate.exchange(requestUrl, HttpMethod.GET, null, String.class);

String responseBody = response.getBody();

System.out.println(responseBody);

}

运行main方法。可以看到程序并没有报错了。

需要注意的是证书是有截止日期的,过期了需要重新导入。5、证书过期了如何再次导入

需要先删除过期的证书,再执行上面的导入指令即可。

6、如何从信任库中删除指定的证书

根据别名删除证书。下面的指令替换成自己的别名和信任库的路径。

keytool -delete -alias subconverter -keystore "C:\Program Files\Java\jdk1.8.0_40\jre\lib\security\cacerts" -storepass changeit

删除成功时并不会有任何提示。 验证证书已经被删除,同样的方法有证书不会报错,删掉证书又报错了。 直接修改 Java 信任库的方式存在一些潜在的缺点:

1、系统依赖性: 修改 Java 信任库需要对目标系统具有足够的权限。在某些情况下,可能需要以管理员或超级用户身份才能执行此操作。

2、全局影响: 修改 Java 信任库是全局性的操作,会影响到整个 Java 运行时环境的安全性。如果添加了不受信任的证书或者不当地修改了信任库,可能会导致安全风险。

3、维护困难: 直接修改 Java 信任库可能会导致维护困难,特别是在多个环境或团队合作的情况下。由于信任库的修改是全局性的,因此需要确保对所有系统和开发人员都能够进行相同的修改。

4、安全性问题: 如果不小心添加了恶意证书或者不受信任的证书,可能会导致安全漏洞。因此,在修改信任库时需要格外小心,确保只添加了可信任的证书。 5、不利于团队开发:因为jdk是安装在本地的,java信任库也是在自己电脑上,很难进行管理。

keytool 工具是干嘛的,是谁提供的

keytool 工具是一个用于管理密钥库和证书的 Java 工具。它是 Java 开发工具包(JDK)的一部分,由 Oracle Corporation 提供。

keytool 主要用于以下几个方面:

生成密钥对和证书请求: keytool 可以生成公钥/私钥对,并创建证书请求(CSR),用于向证书颁发机构(CA)请求签发数字证书。导入和导出证书: keytool 可以用于导入和导出 X.509 证书。您可以使用它从证书文件中导入证书到密钥库中,或者导出密钥库中的证书到文件中。管理密钥库: keytool 可以用于创建、查看、更新和删除密钥库中的条目,例如密钥对、证书、证书链等。管理信任库: keytool 可以用于管理 Java 的信任库,包括添加、删除和查看信任库中的受信任证书。

下面出现其他的解决方案,用来解决上面的问题。

3、手动创建信任库-添加证书 - 推荐

既然使用Java的信任库有诸多缺点,那么使用自己创建的信任库就没有那么多问题了。

1、创建信任文件并导入证书。注意修改证书路径为自己的。

keytool -import -file "E:\idea项目\sifanERP\h3yun-api\src\main\resources\subconverter.hladder.xyz.crt" -alias subconverter -keystore "E:\idea项目\sifanERP\h3yun-api\src\main\resources\mycacerts.jks"

命令解释

这个命令使用 keytool 工具来将指定的证书文件导入到一个 Java Keystore (JKS) 格式的信任库中。以下是各个参数的解释:

-import: 表示执行导入操作。-file "E:\idea项目\sifanERP\h3yun-api\src\main\resources\subconverter.hladder.xyz.crt": 指定要导入的证书文件的路径。在这个命令中,证书文件的路径是 “E:\idea项目\sifanERP\h3yun-api\src\main\resources\subconverter.hladder.xyz.crt”。-alias subconverter: 指定别名,用于标识导入的证书。在后续操作中,可以使用这个别名来引用这个证书。-keystore "E:\idea项目\sifanERP\h3yun-api\src\main\resources\mycacerts.jks": 指定要导入的目标 keystore 文件的路径。在这个命令中,指定了一个名为 “mycacerts.jks” 的 keystore 文件,路径为 “E:\idea项目\sifanERP\h3yun-api\src\main\resources”。

使用管理员打开CMD,运行上面的指令,运行过程中会提示需要设置信任库的密码。 为了方便记忆,密码设置为和Java信任库一致,也就是changeit当然也可以随便自己设置一个,但是得记住,因为需要用到密码才能使用这个信任库。

这个信任库是可以导入多个证书的。添加其他的证书时需要输入密码。下面是添加相同的证书它不让看看就好,添加其他证书也是上面一样的指令。 2、使用自己创建的信任证书

为了测试,我已经删除了Java信任库中的证书。

此时运行test2方法是报错的。 下面是使用自己创建的信任证书的方式,方法名为我用test3。

代码如下:

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

TestNPTO testNPTO = new TestNPTO();

testNPTO.test03();

}

void test03() throws Exception {

// 证书文件路径

// 获取资源文件的输入流

InputStream inputStream = this.getClass().getResourceAsStream("/mycacerts.jks");

// 证书密码 - 自己设置的密码

String certificatePassword = "changeit";

// 加载证书文件

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());

keyStore.load(inputStream, certificatePassword.toCharArray());

inputStream.close();

// 创建 TrustManagerFactory 并初始化

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

trustManagerFactory.init(keyStore);

// 获取 TrustManager

TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

// 使用自定义的 TrustManager 来实现证书验证

TrustManager customTrustManager = new X509TrustManager() {

@Override

public void checkClientTrusted(X509Certificate[] chain, String authType) {

// 实现客户端证书验证的逻辑,此处留空,因为我们是客户端

}

@Override

public void checkServerTrusted(X509Certificate[] chain, String authType) {

// 实现服务器证书验证的逻辑,此处留空,因为我们已经在 KeyStore 中加载了特定证书

}

@Override

public X509Certificate[] getAcceptedIssuers() {

return new X509Certificate[0];

}

};

// 将自定义的 TrustManager 添加到 TrustManager 数组中

TrustManager[] customTrustManagers = new TrustManager[trustManagers.length + 1];

System.arraycopy(trustManagers, 0, customTrustManagers, 0, trustManagers.length);

customTrustManagers[trustManagers.length] = customTrustManager;

// 创建 SSLContext 并初始化

SSLContext sslContext = SSLContext.getInstance("TLS");

sslContext.init(null, customTrustManagers, null);

// 使用 SSLContext 来创建 SSLSocketFactory

SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

// 创建 RestTemplate

RestTemplate restTemplate = new RestTemplate();

// 设置自定义的 SSLSocketFactory

restTemplate.setRequestFactory(new SimpleClientHttpRequestFactory() {

@Override

protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {

if (connection instanceof HttpsURLConnection) {

((HttpsURLConnection) connection).setSSLSocketFactory(sslSocketFactory);

}

super.prepareConnection(connection, httpMethod);

}

});

// 发起 HTTPS 请求

String requestUrl = "https://subconverter.hladder.xyz/version";

ResponseEntity response = restTemplate.exchange(requestUrl, HttpMethod.GET, null, String.class);

String responseBody = response.getBody();

System.out.println(responseBody);

}

运行test3,看看效果,可以看到test3成功的。为了使用方便可以把上面的restTemplate封装成一个Component。为了更灵活使用可以把mycacerts.jks放在OSS中或数据库等其他地方,方便证书过期而不需要重新进行系统部署。甚至可以把创建jks也用代码实现,在OSS上只用放证书就行。

下面是对RestTemplate类的封装,Bean的名称为customRestTemplate,如果mycacerts.jks是远程加载的,加载时只需更新下customRestTemplate组件就行。

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.http.client.SimpleClientHttpRequestFactory;

import org.springframework.web.client.RestTemplate;

import javax.net.ssl.*;

import java.io.IOException;

import java.io.InputStream;

import java.net.HttpURLConnection;

import java.security.KeyStore;

import java.security.cert.X509Certificate;

@Configuration

public class RestTemplateConfiguration {

@Bean(name = "customRestTemplate")

public RestTemplate customRestTemplate() throws Exception {

// 证书文件路径

// 获取资源文件的输入流

InputStream inputStream = this.getClass().getResourceAsStream("/mycacerts.jks");

// 证书密码 - 自己设置的密码

String certificatePassword = "changeit";

// 加载证书文件

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());

keyStore.load(inputStream, certificatePassword.toCharArray());

inputStream.close();

// 创建 TrustManagerFactory 并初始化

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

trustManagerFactory.init(keyStore);

// 获取 TrustManager

TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

// 使用自定义的 TrustManager 来实现证书验证

TrustManager customTrustManager = new X509TrustManager() {

@Override

public void checkClientTrusted(X509Certificate[] chain, String authType) {

// 实现客户端证书验证的逻辑,此处留空,因为我们是客户端

}

@Override

public void checkServerTrusted(X509Certificate[] chain, String authType) {

// 实现服务器证书验证的逻辑,此处留空,因为我们已经在 KeyStore 中加载了特定证书

}

@Override

public X509Certificate[] getAcceptedIssuers() {

return new X509Certificate[0];

}

};

// 将自定义的 TrustManager 添加到 TrustManager 数组中

TrustManager[] customTrustManagers = new TrustManager[trustManagers.length + 1];

System.arraycopy(trustManagers, 0, customTrustManagers, 0, trustManagers.length);

customTrustManagers[trustManagers.length] = customTrustManager;

// 创建 SSLContext 并初始化

SSLContext sslContext = SSLContext.getInstance("TLS");

sslContext.init(null, customTrustManagers, null);

// 使用 SSLContext 来创建 SSLSocketFactory

SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

// 创建 RestTemplate

RestTemplate restTemplate = new RestTemplate();

// 设置自定义的 SSLSocketFactory

restTemplate.setRequestFactory(new SimpleClientHttpRequestFactory() {

@Override

protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {

if (connection instanceof HttpsURLConnection) {

((HttpsURLConnection) connection).setSSLSocketFactory(sslSocketFactory);

}

super.prepareConnection(connection, httpMethod);

}

});

return restTemplate;

}

}

下面对customRestTemplate的使用测试

代码如下;

@Resource

private RestTemplate customRestTemplate;

@Test

void test04() throws Exception {

String requestUrl = "https://subconverter.hladder.xyz/version";

ResponseEntity response = customRestTemplate.exchange(requestUrl, HttpMethod.GET, null, String.class);

String responseBody = response.getBody();

System.out.println(responseBody);

}

@Test

void test05() throws Exception {

String requestUrl = "https://subconverter.hladder.xyz/version";

RestTemplate restTemplate = new RestTemplate();

ResponseEntity response = restTemplate.exchange(requestUrl, HttpMethod.GET, null, String.class);

String responseBody = response.getBody();

System.out.println(responseBody);

}

测试结果如下面两张图,使用customRestTemplate的test04方法是OK的,而test05报错。测试成功。 注意:证书过期需要重新搞一下自己的信任库。

4、JVM参数设置信任库

这个方案也是需要手动创建信任库的,创建方法在第三个方案里面。假设现在已经创建好了信任库。名称为mycacerts.jks位于resources目录下。

1、打开 - 修改运行配置Edit Run Configuration

添加JVM参数。 填入参数,注意:信任库的位置填自己的。

-Djavax.net.ssl.trustStore="E:\idea项目\sifanERP\h3yun-api\src\main\resources\mycacerts.jks" -Djavax.net.ssl.trustStorePassword=changeit

点击Apply和OK。

2、运行测试一下。

可以看到运行也是ok的,jvm的参数看到加上了的。

当然我种方式也是有弊端的,就是证书过期需要重新部署应用。

踩坑

maven打包后报错Invalid keystore format

我先执行maven的打包package再执行代码就会报错 报错的代码

keyStore.load(inputStream, certificatePassword.toCharArray());

是由于 Maven 的资源过滤导致的文件格式变化而引起的。对于二进制文件(如 .jks),进行文本过滤可能会破坏文件的格式,导致加载失败。

解决这个问题的方法之一是告诉 Maven 不要对 .jks 文件进行过滤。可以在 Maven 的 pom.xml 文件中配置资源过滤的排除规则,确保 .jks 文件不会被过滤。

1、方案1 -推荐 在resources目录下新建一个目录jks,把jks文件全放jks目录下面去,同时这样也有助于管理jks文件。

Maven 会将资源文件复制到target/classes目录中,但是它只会对src/main/resources目录下的文件进行过滤,不会对src/main/resources目录下的文件夹进行过滤。

此时重新打包看下效果。可以看到打包过后并没有报错。

2、方案2 在pom.xml文件中进行如下配置:作用是maven过滤时忽略.jks结尾的文件。注意:需要同时写上过滤哪些和不过滤那些,最好写在父项目的pom.xml中。我建议使用方案1是最佳的。

src/main/resources

true

application.yml

application.yaml

application.properties

bootstrap.yml

bootstrap.yaml

bootstrap.properties

src/main/resources

false

*.jks

测试结果也是没有问题的。

推荐阅读

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。