背景介绍

前面我们在一文带你快速掌握java http客户端的证书校验中讲过一些对安全性要求比较高的公司会要求双向认证,比如银行。双向认证就是除了客户端去校验服务器的证书外,服务器也会去校验客户端的身份,防止黑客冒充银行面向用户,或者冒充用户面向银行。

话不多说,咱们开始吧。需要注意的是:本文案例基于JDK17,建议小伙伴们在学习本篇文章时和我的JDK版本保持一致! 下面我们来看下在springboot应用中如何实现双向认证。

在springboot应用中实现双向认证

双向认证和单向的客户端验证服务器证书不同,双向认证需要手动生成根证书,因为单向认证是开放的,证书是公开给所有客户端,其背后是整个互联网基础设施做支撑。而双向认证是封闭的,它只是一个机构内部的互信机制 ,因此需要手动生成根证书,只在公司内部使用。另外CA机构不可能给每个客户端都生成一个证书,因为客户端的证书不是面向整个互联网的,它只是为了给特定服务器出示自己的身份。

打个比方,单向认证就好像你作为顾客和企业打交道,对方的信用背书是政府,是工商局。而双向认证则是一个公司内部的行为,员工通过公司的营业执照来确定这是自己正在为之努力的公司,而公司也需要通过你佩戴的工牌确认你是它内部的员工。

如果我这段解释不能让你满意的话,建议你去问下AI,我感觉它讲的比我更清楚(笑哭)。总而言之,服务器需要根证书来验证客户端的身份,服务端的证书和客户端的证书都是使用根证书下发的。

生成根证书

首先我们需要生成根证书,进入到下图目录的cmd终端
在这里插入图片描述
然后执行如下指令:

# 生成 ECC 私钥(secp384r1 曲线),执行该指令后,当前目录会多出一个名为ca.key的文件,这个文件就是根证书的私钥
"C:\Program Files\OpenSSL-Win64\bin\openssl.exe" ecparam -name secp384r1 -genkey -out ca.key

# 生成自签名的 CA 根证书,执行该指令后,当前目录会多出一个名为ca.crt的文件,这个文件就是根证书
"C:\Program Files\OpenSSL-Win64\bin\openssl.exe" req -new -x509 -days 365 -key ca.key -out ca.crt -subj "/CN=MyCA-ECC" -addext "basicConstraints=critical,CA:TRUE"

# 创建一个信任库,并且将根证书放到该信任库中,执行该指令后,当前目录会多出一个名为ca.p12的文件,这个文件就是信任库文件,将来服务器通过这个信任库来验证客户端的身份,而且该信任库的密码是123123
"C:\Program Files\Java\jdk-17\bin\keytool.exe" -importcert -alias ca -file ca.crt -keystore ca.p12 -storetype PKCS12 -storepass 123123 -noprompt

执行上面的执行后,我们会在当前目录下看到多出了这3个文件:

  1. ca.key:根证书的秘钥
  2. ca.crt:根证书
  3. ca.p12:存有根证书的信任库,将来服务器靠这个信任库去验证客户端的身份

执行如下指令查看ca.p12信任库中的内容:

# 查看ca.p12信任库中的信息,
"C:\Program Files\Java\jdk-17\bin\keytool.exe" -list -v -keystore ca.p12 -storepass 123123

执行结果如下:
在这里插入图片描述
里面可以看到根证书的别名,以及使用什么算法生成的根证书等等。

做完了这些后,我们就需要使用前面生成的根证书,根秘钥去生成服务器的证书和秘钥了。

生成服务端的密钥库

同样是在刚才的cmd终端,继续执行如下指令:

# 生成服务端 ECC 私钥,执行该指令后,当前目录下会多出一个名为server.key的文件,这个文件就是服务器的私钥
"C:\Program Files\OpenSSL-Win64\bin\openssl.exe" ecparam -name secp384r1 -genkey -out server.key

# 生成 CSR(证书签名请求),使用前面生成的私钥签名,生成CSR,需要注意的是CN=localhost这里CN的值一定要填写服务器的域名,不要瞎填。执行该指令后,当前目录下会多出一个server.csr文件
"C:\Program Files\OpenSSL-Win64\bin\openssl.exe" req -new -key server.key -out server.csr -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

在当前目录下创建名为extensions.cnf的文件,文件内容如下:

subjectAltName=DNS:localhost,IP:127.0.0.1

继续在cmd终端执行如下指令:

# 用 CA 签发服务端证书。这里会用到前面的extensions.cnf文件,这个指令的作用是使用根证书,根秘钥,以及前面生成的csr来生成服务器的证书,执行该指令后,当前目录下会多出一个server.crt文件
"C:\Program Files\OpenSSL-Win64\bin\openssl.exe" x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -extfile extensions.cnf

# 创建服务器的密钥库,并且将前面生成的服务器证书,服务器私钥,以及根证书放到密钥库中。在springboot应用中需要这个东西,密钥库的密码为123456,别名为server-ssl。执行该指令后,当前目录下会多出一个server.p12文件,这就是密钥库
"C:\Program Files\OpenSSL-Win64\bin\openssl.exe" pkcs12 -export -inkey server.key -in server.crt -certfile ca.crt -out server.p12 -password pass:123456 -name "server-ssl"

总而言之,最终我们执行上述指令后会多出如下文件:

  1. server.key:服务器的私钥
  2. server.csr:服务器的证书请求文件
  3. extensions.cnf:扩展文件
  4. server.crt:服务器的证书
  5. server.p12:服务器的密钥库文件

接下来我们执行如下指令查看密钥库中的信息:

# 查看秘钥库中的信息
"C:\Program Files\Java\jdk-17\bin\keytool.exe" -list -v -keystore server.p12 -storepass 123456

可以看到该密钥库中有一个别名为server-ssl的证书
在这里插入图片描述
接下来我们还需要为客户端生成证书和秘钥,因为服务器也需要认证客户端的身份。

生成客户端端的密钥库

生成客户端密钥库的整个过程跟前面生成服务端密钥库一样。在当前的cmd终端继续执行如下指令:

# 生成客户端 ECC 私钥
"C:\Program Files\OpenSSL-Win64\bin\openssl.exe" ecparam -name secp384r1 -genkey -out client.key

# 生成 CSR
"C:\Program Files\OpenSSL-Win64\bin\openssl.exe" req -new -key client.key -out client.csr -subj "/CN=Client1-ECC"

# 用 CA 签发客户端证书
"C:\Program Files\OpenSSL-Win64\bin\openssl.exe" x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt

# 创建密钥库,并且将客户端证书,根证书,客户端秘钥给放进去,执行该指令后,当前目录下会多出一个client.p12文件,这个就是客户端的密钥库文件,后续客户端使用这个密钥库实现和服务器的通信,用来向服务器证明自己的身份
"C:\Program Files\OpenSSL-Win64\bin\openssl.exe" pkcs12 -export -out client.p12 -inkey client.key -in client.crt -certfile ca.crt -password pass:123123

执行上述指令后,当前目录下将会多出如下文件:

  1. client.key:客户端的私钥
  2. client.csr:客户端请求生成证书的文件
  3. client.csr:客户端的证书
  4. client.p12:客户端的密钥库,也是客户端发送请求时需要的东西

接下来,我们查看下 client.p12密钥库中的内容,执行如下指令

# 查看密钥库中的内容
"C:\Program Files\Java\jdk-17\bin\keytool.exe" -list -v -keystore client.p12 -storepass 123123

执行结果如下图:
在这里插入图片描述
执行上述所有指令后,目录下应该会有这些文件:
在这里插入图片描述

配置springboot双向认证

接下来就需要在springboot应用中配置双向认证了,会用到前面的一些信息,比如服务器的密钥库,含有根证书的信任库等等。具体配置如下:

server:
  port: 443
# 开启https,下面的配置需要和密钥库文件的配置保持一致
  ssl:
    # 密钥库的配置
    enabled: true
    key-store-type: PKCS12 # 密钥库的类型
    key-store-password: 123456 # 密钥库的密码
    key-alias: server-ssl # 密钥库中证书的别名
    key-store: classpath:client/server.p12 # 密钥库文件

    # 信任库的配置,需要注意的是trust-store-type的类型不能使用PEM,否则会报错
    trust-store-type: PKCS12
    trust-store: classpath:client/ca.p12 # 信任的CA(用于验证客户端)
    trust-store-password: 123123 # 信任库的密码
    client-auth: need  # 强制双向认证
    protocol: TLS   # 使用 TLS 1.2/1.3
    enabled-protocols: TLSv1.3,TLSv1.2
    ciphers:  # 指定安全的加密套件
      - TLS_AES_256_GCM_SHA384        # TLS 1.3
      - TLS_CHACHA20_POLY1305_SHA256  # TLS 1.3
      - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384  # TLS 1.2

配置完成后,重启springboot应用,接下来我们开始测试服务器和客户端的双向认证。

测试双向认证

使用ApiFox测试双向认证

由于浏览器不会自动发送客户端的证书,所以我们就不能使用浏览器去测试了。curl指令也不行,我试过了,使用curl指令会失败。折腾了半天,发现还是ApiFox好使,所以接下来我将带大家使用ApiFox来测试服务器和客户端的双向认证。

直接发起请求会失败,因为没有配置服务器证书和客户端证书,如下图所示:
在这里插入图片描述
点击【证书管理】配置服务器证书,也就是我们之前生成的server.csr,如下图所示:
在这里插入图片描述
需要注意的是:这里上传服务器的证书或者根证书都是可以的,因为最终都是在校验对方的根证书!如下图所示:
在这里插入图片描述
然后继续配置客户端证书:
在这里插入图片描述
最后点击【添加】
在这里插入图片描述
同时添加服务器证书和客户端信任库后,接口终于调通了,没枉费我一片苦心,这里的测试结果说明我们的双向认证的配置是有效的。到这里,客户端和服务器的双向认证就讲完了。

需要注意的是,ApiFox要同时配置好服务器证书和客户端证书,少其中任何一个都会导致接口无法调通。要不然怎么说叫双向认证。

使用java的http客户端验证双向认证

除了使用ApiFox外,我们还可以使用java的http客户端来测试双向认证,代码如下:

package com.lizemin.truststore;

import org.junit.jupiter.api.Test;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;

/**
 * @author lzm
 * @date 2025/10/5 21:03
 * @description
 */
public class ClientVerifyTest {


    /**
     * 验证客户端和服务端的双向认证
     */
    @Test
    public void test_client_verify() throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException, InterruptedException {
        // 构建ssl context,这是加密的核心
        SSLContext sslContext = getSslContext();

        // 6. 创建 HttpClient
        HttpClient client = HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_1_1)
                .sslContext(sslContext)
                .build();

        // 7. 发送请求
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://localhost/csdn/hello")) // 替换为你的接口URL
                .GET()
                .build();

        HttpResponse<String> response = client.send(
                request,
                HttpResponse.BodyHandlers.ofString()
        );

        System.out.println("Status Code: " + response.statusCode());
        System.out.println("Response Body: " + response.body());
    }

    private static SSLContext getSslContext() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException {
        // 加载客户端的密钥库(含有客户端私钥和证书)
        KeyManagerFactory keyManagerFactory = getKeyManagerFactory();

        // 将服务器的证书添加到客户端的信任库中,用于校验服务端的证书
        TrustManagerFactory trustManagerFactory = getTrustManagerFactory();

        // 5. 配置 SSLContext
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(
                keyManagerFactory.getKeyManagers(), // 客户端密钥库
                trustManagerFactory.getTrustManagers(), //信任库
                new SecureRandom()
        );
        return sslContext;
    }

    private static KeyManagerFactory getKeyManagerFactory() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException {
        // 1. 加载客户端证书和私钥, 这里选择是加载打包成密钥库形式的文件,包含了这两样东西
        Path clientKeystorePath = Paths.get("src/main/resources/client/client.p12");
        char[] clientKeystorePassword = "123123".toCharArray();
        KeyStore clientKeyStore = KeyStore.getInstance("PKCS12");
        clientKeyStore.load(Files.newInputStream(clientKeystorePath), clientKeystorePassword);

        // 2. 初始化 KeyManagerFactory
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX");
        keyManagerFactory.init(clientKeyStore, clientKeystorePassword);
        return keyManagerFactory;
    }

    /**
     * 为客户端添加信任库,用于校验服务器证书的合法性
     */
    private static TrustManagerFactory getTrustManagerFactory() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
        // 3. 加载信任的CA证书(服务端根证书)
        Path truststorePath = Paths.get("src/main/resources/client/ca.crt");
        KeyStore trustStore = KeyStore.getInstance("PKCS12");
        trustStore.load(null, null); // 创建空的TrustStore
        // 将CA证书添加到TrustStore
        try (var caInput = Files.newInputStream(truststorePath)) {
            var caCertificate = CertificateFactory.getInstance("X.509")
                    .generateCertificate(caInput);
            trustStore.setCertificateEntry("ca", caCertificate);
        }

        // 4. 初始化 TrustManagerFactory
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("PKIX");
        trustManagerFactory.init(trustStore);
        return trustManagerFactory;
    }

}

执行单元测试,结果如下图:可以看到请求成功了。
在这里插入图片描述
然后我们注释掉客户端证书,再执行下单元测试,验证下服务器是否真的会去校验客户端的证书,如下图所示:
在这里插入图片描述
执行结果如下图:
在这里插入图片描述
可以看到,注释掉客户端的密钥库后,请求失败了,服务端提示说客户端的证书有问题,这也说明我们的双向认证是有效的!

使用Nginx配置双向认证

一般来说,公司内部都会使用nginx作为负载均衡防止单点故障,所以https和双向认证配置在nginx的情况更多一些,接下来就来教教大家如何在Nginx上配置双向认证。

在nginx的配置文件中增加如下配置即可:
在这里插入图片描述
然后我们使用浏览器验证一下双向认证
在这里插入图片描述
然后我们使用ApiFox验证一下双向认证,同样的按照前面的方式配置好服务端证书和客户端的密钥库。
在这里插入图片描述
发送请求,可以看到请求成功了。
在这里插入图片描述
然后我们去掉客户端证书,再次请求:
在这里插入图片描述

结果如下:报了这个错误,这也说明了nginx确实有在认真的校验客户端的证书是否合法!
在这里插入图片描述

本文完整示例代码的地址

本文完整示例代码的地址
在这里插入图片描述

最后的总结

今天这篇文章主要是给小伙伴们介绍了如何在springboot应用中实现服务器和客户端的双向认证,主要适用于对安全性要求比较高的场景中。对绝大部分网站来说,单向认证就够用了,所以这篇文章大家看下就行了,就当是开阔一下自己的眼界。

最后我们还讲了如何使用nginx实现双向认证,在nginx中实现双向认证后,各个应用服务就不要再去配置双向认证了。我个人认为还是nginx配置双向认证要方便很多,不仅仅是配置层面。你想一下,你是愿意在nginx上搞一次双向认证的配置,还是愿意在n多个应用服务中搞n次双向认证的配置?

觉得有收获的朋友可以点个赞,您的鼓励就是我最大的动力!

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐