HTTP协议到HTTPS的Java客户端改造

前言

由于安全原因,我们公司对外暴露的接口通过HTTP协议的方式在未来的某一天将被彻底关闭。

从那以后,外部客户在调用我公司的接口时就只能通过HTTPS协议。

本篇文章的目的就是安全的指导外部客户的客户端开发人员或者有类似需求的Java开发人员,如何从HTTP协议调用改造为通过HTTPS来进行调用。(本篇文章只提供Java的处理方式,其他编程语言大同小异可以自行搜索)

例子

假设这是原来的调用接口文档的代码示例

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 复制代码
http://api.example-fake-domain.com/oauth/client/token

修改为

http 复制代码
https://api.example-fake-domain.com/oauth/client/token

PS:当然,不仅仅是这一个地址,之前文档里提供的全部地址都要进行类似的改动,即从http://改为https://

但这还远远不够。因为此时运行程序会有报错,即

java 复制代码
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里)

执行以下命令
sh 复制代码
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 可以直接处理该格式。
执行这个命令后的步骤
  1. 输入 keystore 密码 :命令执行后,系统会提示你输入 keystore 的密码。默认密码通常是 changeit
  2. 确认导入证书 :在命令执行过程中,keytool 会显示证书的详细信息,并询问你是否信任该证书。输入 yes 确认导入。
  3. 成功导入 :如果操作成功,keytool 会提示证书已成功添加到 keystore 中。

至此,报错解除,再次启动,也不再报错了。

PS: 权限问题 :在某些系统上,特别是 macOS 和 Linux 上,可能需要使用 sudo 提升权限执行该命令,具体取决于 cacerts 文件的权限设置。

如果需要删除证书,可以参考附录。

二. 使用自定义 TrustStore

如果你上面的步骤已经成功,那么后面的内容也就没有必要看了。

但如果你实在没有权限修改全局的 Java cacerts 信任库,也有替代方法可以让你的 Java 应用程序信任指定的证书。即使用自定义 TrustStore

1. 生成自定义 TrustStore

可以创建一个自定义的 TrustStore 文件,并将证书导入到这个 TrustStore 中,然后在运行 Java 应用程序时指定使用这个 TrustStore。(此时假设你已经获取到ssl证书文件,如果没有可以参考上面【获取服务器的 SSL 证书】的操作)

sh 复制代码
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:

sh 复制代码
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 证书】的操 作)

java 复制代码
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 信任库中删除。以下是详细步骤:

删除证书的命令

bash 复制代码
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 的安装目录。你可以使用实际路径或保持这个变量格式。
执行删除操作后的步骤
  1. 输入 keystore 密码 :执行命令后,会提示你输入 keystore 的密码。默认密码通常是 changeit

  2. 确认删除 :执行命令后,keytool 会删除指定别名下的证书,并在成功后提示证书已被删除。

注意事项
  • 别名必须匹配 :确保你删除的别名与导入时使用的别名完全一致。如果别名不匹配,keytool 无法找到要删除的证书。

  • 权限问题 :在某些系统上,特别是 macOS 和 Linux 上,可能需要使用 sudo 提升权限执行该命令。

    例如:

    bash 复制代码
    sudo keytool -delete -alias fake -keystore $JAVA_HOME/jre/lib/security/cacerts
  • 删除后重启应用程序:删除证书后,可能需要重启 Java 应用程序或 IDE,以确保更改生效。

通过执行这些步骤,你可以轻松地将已经导入的证书从 Java 的信任库中删除。

相关推荐
雷神乐乐5 分钟前
File.separator与File.separatorChar的区别
java·路径分隔符
小刘|9 分钟前
《Java 实现希尔排序:原理剖析与代码详解》
java·算法·排序算法
逊嘘28 分钟前
【Java语言】抽象类与接口
java·开发语言·jvm
morris13135 分钟前
【SpringBoot】Xss的常见攻击方式与防御手段
java·spring boot·xss·csp
‍。。。1 小时前
使用Rust实现http/https正向代理
http·https·rust
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员1 小时前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU1 小时前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie61 小时前
在IDEA中使用Git
java·git
Elaine2023911 小时前
06 网络编程基础
java·网络