赞
踩
在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<String> 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程序仍然可能会出现证书验证错误,导致无法建立安全连接。
下面是提供了如何解决这个问题的方案。
代码如下
// 创建一个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<String> response = restTemplate.exchange(requestUrl, HttpMethod.GET, null, String.class); // 获取响应体 String responseBody = response.getBody(); // 输出响应体内容 System.out.println(responseBody);
运行代码之后成功获取到了结果,并且没有报错。
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<String> 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 的信任库,包括添加、删除和查看信任库中的受信任证书。
下面出现其他的解决方案,用来解决上面的问题。
既然使用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<String> 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<String> 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<String> response = restTemplate.exchange(requestUrl, HttpMethod.GET, null, String.class); String responseBody = response.getBody(); System.out.println(responseBody); }
测试结果如下面两张图,使用customRestTemplate的test04方法是OK的,而test05报错。测试成功。
注意:证书过期需要重新搞一下自己的信任库。
这个方案也是需要手动创建信任库的,创建方法在第三个方案里面。假设现在已经创建好了信任库。名称为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的打包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是最佳的。
<build> <!-- Maven 构建配置 --> <resources> <resource> <directory>src/main/resources</directory> <!--引入所需环境的配置文件--> <filtering>true</filtering> <includes> <include>application.yml</include> <include>application.yaml</include> <include>application.properties</include> <include>bootstrap.yml</include> <include>bootstrap.yaml</include> <include>bootstrap.properties</include> </includes> </resource> <resource> <!-- 资源文件目录 --> <directory>src/main/resources</directory> <!-- 禁用对所有资源文件的过滤操作 --> <filtering>false</filtering> <!-- 包含所有类型的资源文件 --> <includes> <include>*.jks</include> </includes> </resource> </resources> </resources> </build>
测试结果也是没有问题的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。