文章目录
前言
在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程序仍然可能会出现证书验证错误,导致无法建立安全连接。
下面是提供了如何解决这个问题的方案。
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<String> response = restTemplate.exchange(requestUrl, HttpMethod.GET, null, String.class);
// 获取响应体
String responseBody = response.getBody();
// 输出响应体内容
System.out.println(responseBody);
运行代码之后成功获取到了结果,并且没有报错。
但可能在某些环境下仍然无法绕过证书验证,使用以下的代码设置请求工厂。
// 禁用SSL证书验证
TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[0];
}
@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());
// 创建RestTemplate并设置请求工厂
CloseableHttpClient httpClient = HttpClients.custom()
.setSSLContext(sslContext)
.setSSLHostnameVerifier(new NoopHostnameVerifier())
.build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);
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<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 的信任库,包括添加、删除和查看信任库中的受信任证书。
下面出现其他的解决方案,用来解决上面的问题。
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<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报错。测试成功。
注意:证书过期需要重新搞一下自己的信任库。
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是最佳的。
<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>
测试结果也是没有问题的。