HTTP协议到HTTPS的Java客户端改造
前言
由于安全原因,我们公司对外暴露的接口通过HTTP协议的方式在未来的某一天将被彻底关闭。
从那以后,外部客户在调用我公司的接口时就只能通过HTTPS协议。
本篇文章的目的就是安全的指导外部客户的客户端开发人员或者有类似需求的Java开发人员,如何从HTTP协议调用改造为通过HTTPS来进行调用。(本篇文章只提供Java的处理方式,其他编程语言大同小异可以自行搜索)
例子
假设这是原来的调用接口文档的代码示例
public static void main(String[] args) {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("client_id", CLIENT_ID);
map.add("client_secret", CLIENT_SECRET);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
ResponseEntity<String> response = restTemplate.postForEntity("http://api.example-fake-domain.com/oauth/client/token", request, String.class);
System.out.println(response.getBody());
}
第一步
那么其中最关键的一步,其中的调用地址需要从
http://api.example-fake-domain.com/oauth/client/token
修改为
https://api.example-fake-domain.com/oauth/client/token
PS:当然,不仅仅是这一个地址,之前文档里提供的全部地址都要进行类似的改动,即从http://改为https://
但这还远远不够。因为此时运行程序会有报错,即
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:439)
at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:306)
at sun.security.validator.Validator.validate(Validator.java:271)
at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:312)
at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:221)
at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:128)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.checkServerCerts(CertificateMessage.java:636)
... 21 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141)
at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126)
at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:280)
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:434)
为什么会报错呢?
1. HTTPS 与 SSL/TLS
HTTPS(HyperText Transfer Protocol Secure)是 HTTP 的加密版本,使用 SSL/TLS(Secure Sockets Layer/Transport Layer Security)协议来加密数据传输。
当你使用 HTTPS 时,客户端(如 Java 程序)和服务器之间的通信会被加密。这种加密需要双方通过一系列步骤(握手协议)来建立一个安全的通信通道.
2. 证书验证
- 在 HTTPS 连接中,服务器会提供一个 SSL/TLS 证书。这个证书由一个被信任的证书颁发机构(CA)签名,包含了服务器的公钥和其他信息。
- 客户端(Java 应用程序)会验证这个证书的有效性,包括验证证书的签发者(CA)、证书链、以及证书是否已经过期等。
3. Java 的证书信任库
- Java 使用一个内置的证书信任库(
cacerts
)来存储受信任的证书颁发机构(CA)的证书。当 Java 应用程序尝试连接到一个 HTTPS 服务器时,它会检查服务器的证书是否由信任库中的 CA 签发。 - 如果服务器提供的证书不在信任库中,或者证书链不完整,Java 会拒绝连接,抛出类似
PKIX path building failed: unable to find valid certification path to requested target
的异常。
解决
那么怎么解决这个报错呢?
有3种方式可以进行处理。
- Java 信任库配置证书
- 使用自定义 TrustStore
- 在程序内指定SSL证书
上面3种方式,只要选择一种即可。可以选择最适合自己项目的方式。
接下来,我将一一对这些方式进行讲解
一. Java 信任库配置证书
1. 获取服务器的 SSL 证书
使用浏览器或命令行工具(如 openssl
)来下载目标服务器的 SSL 证书。以下是通过浏览器获取证书的步骤:
- 使用浏览器访问目标 HTTPS URL。
- 点击地址栏中的锁🔒图标。
- 查看证书信息并导出证书(通常是
.cer
格式或者.pem
)。
为了方便后续的讲解,我们假设你获取下来的文件名为
_.example-fake-domain.com.pem
2. 导入证书到 Java 信任库
首先找到你服务器的 Java 安装目录的 lib/security
目录下。
这个目录通常位于$JAVA_HOME/jre/lib/security/cacerts(如果你这边本地测试的时候,需要注意你的项目启动时使用的是哪个jdk,要到对应目录下的/jre/lib/security/cacerts里)
执行以下命令
keytool -import -alias fake -keystore $JAVA_HOME/jre/lib/security/cacerts -file _.example-fake-domain.com.pem
命令解释
参数 | 含义 | 示例值 | 说明 |
---|---|---|---|
keytool | Java 自带的密钥和证书管理工具,用于管理公钥、私钥和证书。 | keytool | 这是 Java SDK 提供的一个命令行工具,用于生成密钥对、自签名证书、导入和导出证书等。 |
-import | 表示要执行的操作是导入证书到信任库。 | -import | 指示 keytool 将证书导入指定的 keystore 中。 |
-alias | 为导入的证书指定一个别名,用于在 keystore 中标识该证书。 | fake | fake 是你给这个证书指定的别名。别名可以是任意字符串,方便以后管理和引用该证书。 |
-keystore | 指定要导入证书的 keystore 文件位置。 | $JAVA_HOME/jre/lib/security/cacerts | cacerts 是 Java 的默认信任库文件。路径中 $JAVA_HOME 是环境变量,指向 JDK 的安装目录。你可以使用实际路径或保持这个变量格式。 |
-file | 指定要导入的证书文件的路径。 | _.example-fake-domain.com.pem | _.example-fake-domain.com.pem 是你下载的服务器证书文件。 pem 文件格式可以包含证书或证书链,keytool 可以直接处理该格式。 |
执行这个命令后的步骤
- 输入
keystore
密码:命令执行后,系统会提示你输入keystore
的密码。默认密码通常是changeit
。 - 确认导入证书:在命令执行过程中,
keytool
会显示证书的详细信息,并询问你是否信任该证书。输入yes
确认导入。 - 成功导入:如果操作成功,
keytool
会提示证书已成功添加到 keystore 中。
至此,报错解除,再次启动,也不再报错了。
PS: 权限问题:在某些系统上,特别是 macOS 和 Linux 上,可能需要使用 sudo
提升权限执行该命令,具体取决于 cacerts
文件的权限设置。
如果需要删除证书,可以参考附录。
二. 使用自定义 TrustStore
如果你上面的步骤已经成功,那么后面的内容也就没有必要看了。
但如果你实在没有权限修改全局的 Java cacerts
信任库,也有替代方法可以让你的 Java 应用程序信任指定的证书。即使用自定义 TrustStore
1. 生成自定义 TrustStore
可以创建一个自定义的 TrustStore 文件,并将证书导入到这个 TrustStore 中,然后在运行 Java 应用程序时指定使用这个 TrustStore。(此时假设你已经获取到ssl证书文件,如果没有可以参考上面【获取服务器的 SSL 证书】的操作)
keytool -import -alias fake -keystore /path/to/your/custom_truststore.jks -file _.example-fake-domain.com.pem
命令含义如下:
参数/选项 | 含义 | 示例值 | 说明 |
---|---|---|---|
keytool | Java 自带的密钥和证书管理工具,用于管理公钥、私钥和证书。 | keytool | 这是 Java SDK 提供的一个命令行工具,用于生成密钥对、自签名证书、导入和导出证书等。 |
-import | 表示要执行的操作是导入证书到信任库。 | -import | 告诉 keytool 将指定的证书导入到 TrustStore 中。 |
-alias | 为导入的证书指定一个别名,用于在 TrustStore 中标识该证书。 | fake | fake 是你给这个证书指定的别名。别名可以是任意字符串,方便以后管理和引用该证书。 |
-keystore | 指定要导入证书的 TrustStore 文件位置。 | /path/to/your/custom_truststore.jks | TrustStore 是一个存储信任证书的文件。这里指定的是自定义的 TrustStore 文件路径,你可以选择你希望存放 TrustStore 的位置和文件名。 |
-file | 指定要导入的证书文件的路径。 | _.example-fake-domain.com.pem | _.example-fake-domain.com.pem 是你下载的服务器证书文件路径。此证书将被导入到指定的 TrustStore 中。 pem 文件格式通常包含一个或多个证书。 |
该命令会要求你设置一个新的 keystore 密码。这个由自己指定,在后面步骤要用到。
2.在启动 Java 应用程序时指定 TrustStore
当你启动 Java 应用程序时,通过 JVM 参数指定使用你创建的 TrustStore:
java -Djavax.net.ssl.trustStore=/path/to/your/custom_truststore.jks -Djavax.net.ssl.trustStorePassword=yourpassword -jar your-application.jar
-Djavax.net.ssl.trustStore
指定 TrustStore 文件路径。
-Djavax.net.ssl.trustStorePassword
指定 TrustStore 密码。
至此启动后,相关报错也不再报了。问题解决。
三.在程序内指定SSL证书
或者,如果你既不配置信任库,也不使用TrustStore。那你也可以在程序内,直接指定pem证书。但是使用时,需要把pem文件放置在对应位置,并修改程序里initSSL里面指定位置的方法。(此时假设你已经获取到ssl证书文件,如果没有可以参考上面【获取服务器的 SSL 证书】的操 作)
import org.example.util.ApiCloudRestTemplateFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
public class ApicloudRestTemplate {
public static void main(String[] args) {
initSSL();
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("client_id", ApiCloudRestTemplateFactory.CLIENT_ID);
map.add("client_secret", ApiCloudRestTemplateFactory.CLIENT_SECRET);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
ResponseEntity<String> response = restTemplate.postForEntity("https://api.example-fake-domain.com/oauth/client/token", request, String.class);
System.out.println(response.getBody());
}
public static void initSSL() {
try {
// 加载自定义证书
FileInputStream fis = new FileInputStream("/Users/admin/_.example-fake-domain.com.pem");
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate caCert = (X509Certificate) cf.generateCertificate(fis);
// 创建一个 KeyStore 并将证书添加到其中
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null); // 初始化空的 KeyStore
ks.setCertificateEntry("fake", caCert);
// 使用 TrustManagerFactory 来创建 TrustManager
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
// 初始化 SSLContext 并设置自定义的 TrustManager
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
SSLContext.setDefault(sslContext);
} catch (IOException | KeyStoreException | KeyManagementException | NoSuchAlgorithmException |
CertificateException e) {
throw new RuntimeException("初始化SSL失败", e);
}
}
}
至此,再次启动,也不再报错了。
四.绝对不要忽略 SSL 验证
在开发环境中,临时忽略 SSL 验证也是一种解决方案,但千万不要在生产环境中使用,因为这会使你的应用程序暴露于中间人攻击。(我就不提供这种方式的相关操作了,太过于危险了)
如果使用忽略SSL认证而造成了任何损失,那么我是概不负责的。
附录
如果您可能由于操作错误,将错误的证书导入了信任库,例如:将不信任的证书错误地导入。或者服务器或服务更换了证书或 CA,新的证书需要替换旧的证书。
可以使用 keytool
工具将已经导入的证书从 Java 的 cacerts
信任库中删除。以下是详细步骤:
删除证书的命令
keytool -delete -alias fake -keystore $JAVA_HOME/jre/lib/security/cacerts
命令参数的详细解释
参数 | 含义 | 示例值 | 说明 |
---|---|---|---|
keytool | Java 自带的密钥和证书管理工具,用于管理公钥、私钥和证书。 | keytool | 这是 Java SDK 提供的一个命令行工具,用于生成密钥对、自签名证书、导入和导出证书等。 |
-delete | 表示要执行的操作是删除证书。 | -delete | 指示 keytool 将指定的证书从 keystore 中删除。 |
-alias | 用于标识要删除的证书的别名。 | fake | fake 是你在导入证书时指定的别名。必须与导入时的别名完全一致。 |
-keystore | 指定包含要删除的证书的 keystore 文件位置。 | $JAVA_HOME/jre/lib/security/cacerts | cacerts 是 Java 的默认信任库文件。路径中 $JAVA_HOME 是环境变量,指向 JDK 的安装目录。你可以使用实际路径或保持这个变量格式。 |
执行删除操作后的步骤
-
输入
keystore
密码:执行命令后,会提示你输入keystore
的密码。默认密码通常是changeit
-
确认删除:执行命令后,
keytool
会删除指定别名下的证书,并在成功后提示证书已被删除。
注意事项
-
别名必须匹配:确保你删除的别名与导入时使用的别名完全一致。如果别名不匹配,
keytool
无法找到要删除的证书。 -
权限问题:在某些系统上,特别是 macOS 和 Linux 上,可能需要使用
sudo
提升权限执行该命令。例如:
sudo keytool -delete -alias fake -keystore $JAVA_HOME/jre/lib/security/cacerts
-
删除后重启应用程序:删除证书后,可能需要重启 Java 应用程序或 IDE,以确保更改生效。
通过执行这些步骤,你可以轻松地将已经导入的证书从 Java 的信任库中删除。