Springboot集成Https双向验证

目的

如果现在当前系统需要开放一个接口 /say-hello 给第三方应用调用。对于认证可以有两种方式:

  • 提供username/password认证后返回token,当访问接口时携带token访问
  • 接口是通过客户端证书来分配权限的,把客户端的cert导入到服务端,客户端通过配置认证才能访问

本文章将讨论「如何使用客户端证书」访问接口。

概念

自签名证书

自签名证书是由不受信的CA机构颁发的数字证书,也就是自己签发的证书。与受信任的CA签发的传统数字证书不同,自签名证书是由一些公司或软件开发商创建、颁发和签名的。虽然自签名证书使用的是与X.509证书相同的加密密钥对架构,但是却缺少受信任第三方(如Sectigo)的验证。

创建自签名SSL证书

1、生成私钥
要创建SSL证书,需要私钥和证书签名请求(CSR)。您可以使用一些生成工具或向CA申请生成私钥,私钥是使用RSA和ECC等算法生成的加密密钥。生成RSA私钥的代码示例:openssl genrsa -aes256 -out servername.pass.key 4096,随后该命令会提示您输入密码。

2、生成CSR
私钥生成后,您的私钥文件现在将作为 servername.key 保存在您的当前目录中,并将用于生成CSR。自签名证书的CSR的代码示例:openssl req -nodes -new -key servername.key -out servername.csr。然后需要输入几条信息,包括组织、组织单位、国家、地区、城市和通用名称。通用名称即域名或IP地址。
输入此信息后,servername.csr 文件将位于当前目录中,其中包含 servername.key 私钥文件。

3、颁发证书
最后,使用server.key(私钥文件)和server.csr 文件生成新证书(.crt)。以下是生成新证书的命令示例:openssl x509 -req -sha256 -days 365 -in servername.csr -signkey servername.key -out servername.crt。最后,在您的当前目录中找到servername.crt文件即可。

参考链接:https://segmentfault.com/a/1190000040834174

SSL证书格式

  • .DER .CER,文件是二进制格式,只保存证书,不保存私钥。
  • .PEM,一般是文本格式,可保存证书,可保存私钥。
  • .CRT,可以是二进制格式,可以是文本格式,与 .DER 格式相同,不保存私钥。
  • .PFX .P12,二进制格式,同时包含证书和私钥,一般有密码保护。
  • .JKS,二进制格式,同时包含证书和私钥,一般有密码保护。
    参考链接:
    https://blog.csdn.net/farrellcn/article/details/119779348

对称加密与非对称加密

  • 对称加密(Symmetric Cryptography)
    对称加密是最快速、最简单的一种加密方式,加密(encryption)与解密(decryption)用的是同样的密钥(secret key)。
  • 非对称加密(Asymmetric Cryptography)
    非对称加密为数据的加密与解密提供了一个非常安全的方法,它使用了一对密钥,公钥(public key)和私钥(private key)。私钥只能由一方安全保管,不能外泄,而公钥则可以发给任何请求它的人。非对称加密使用这对密钥中的一个进行加密,而解密则需要另一个密钥。

常用的命令

  • OpenSSL是一个开放源代码的软件库包,应用程序可以使用这个包来进行安全通信,避免窃听,同时确认另一端连接者的身份。这个包广泛被应用在互联网的网页服务器上。
  • keytool 是个密钥和证书管理工具。它使用户能够管理自己的公钥/私钥对及相关证书,用于(通过数字签名)自我认证(用户向别的用户/服务认证自己)或数据完整性以及认证服务。它还允许用户储存他们的通信对等者的公钥(以证书形式)。
  • JKS和PKCS#12:都是比较常用的两种密钥库格式/标准。对于前者,搞Java开发,尤其是接触过HTTPS平台的朋友,并不陌生。JKS文件(通常为.jks或.keystore,扩展名无关)可以通过Java原生工具——KeyTool生成;而后者PKCS#12文件(通常为.p12或.pfx,意味个人信息交换文件),则是通过更为常用的OpenSSL工具产生。
    当然,这两者之间是可以通过导入/导出的方式进行转换的!当然,这种转换需要通过KeyTool工具进行!

生成私钥private.key

openssl genrsa -out private.key 2048

生成用于申请请求的证书文件csr,一般会将该文件发送给CA机构进行认证,本例使用自签名证书request.csr

openssl req -new -key private.key -out request.csr

自签名证书root.crt

openssl x509 -req -days 365 -in request.csr -signkey private.key -out root.crt

查看证书信息

openssl x509 -noout -text -in root.crt

openssl生成p12文件cert.p12

openssl pkcs12 -export -out cert.p12 -inkey private.key -in root.crt

p12文件转换为keystore(jks)文件my.keystore

keytool -importkeystore -srckeystore cert.p12 -destkeystore my.keystore -deststoretype pkcs12

keytool生成p12文件ssl-key.p12

keytool -genkeypair -alias serverssl -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore ssl-key.p12 -validity 3650

生成keystore(jks)文件

keytool -genkeypair -dname "cn=clientAuth_vmi_th, ou=IS, o=SGM, c=CN" -alias serverssl -keypass Pass1234 -keystore serverssl.jks -validity 3600 -keyalg RSA -keysize 2048 -sigalg SHA256WithRSA

查看密钥库

keytool -list -rfc --keystore clientAuth_vmi_th.jks -storepass Pass1234

密钥库导出别名clientAuthCert证书clientAuthCert.cer

keytool -export -file clientAuthCert.cer -keystore clientAuthCert.jks -storepass Pass1234 -alias clientAuthCert

keytool -export -file clientAuthCert.cer -keystore clientAuthCert.p12 -storepass Pass1234 -alias clientAuthCert

导入证书到密钥库

keytool -import -alias localhost -file localhost.cer -keystore client.jks

参考链接:
https://www.cnblogs.com/qq931399960/p/11889349.html
http://t.zoukankan.com/zwh0910-p-15214672.html
https://www.cnblogs.com/kabi/p/6232966.html
https://www.jianshu.com/p/e83150254ef8
https://zhuanlan.zhihu.com/p/406815419

示例

后端代码SpringBoot

application.yml

server:
# p12密钥匙库:keytool -genkeypair -alias serverssl -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore ssl-key.p12 -validity 3650
#  ssl:
#    key-store: classpath:ssl-key.p12
#    key-store-password: Pass1234
#    keyStoreType: PKCS12
#    keyAlias: serverssl
#    client-auth: need

# JKS密钥匙库:keytool -genkeypair -dname "cn=clientAuth_vmi_th, ou=IS, o=SGM, c=CN" -alias clientAuthCert -keypass Pass1234 -keystore clientAuthCert.jks -storepass Pass1234 -validity 3600 -keyalg RSA -keysize 2048 -sigalg SHA256WithRSA
  ssl:
    key-store: classpath:clientAuth_vmi_th.jks
    key-store-password: Pass1234
    keyStoreType: JKS
    keyAlias: clientAuthCert
    key-password: Pass1234

    client-auth: need # 需要客户端认证

  port: 8443

SslApplication.java

@SpringBootApplication
@RestController
public class SslApplication {

    public static void main(String[] args) {
        SpringApplication.run(SslApplication.class, args);
    }

    @GetMapping("say-hello")
    public String sayHello() {
        return "hello";
    }

}

ServerConfig.java http跳转https

@Configuration
public class ServerConfig {

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint securityConstraint = new SecurityConstraint();
                securityConstraint.setUserConstraint("CONFIDENTIAL");
                SecurityCollection collection = new SecurityCollection();
                collection.addPattern("/*");
                securityConstraint.addCollection(collection);
                context.addConstraint(securityConstraint);
            }
        };
        tomcat.addAdditionalTomcatConnectors(getHttpConnector());
        return tomcat;
    }

    private Connector getHttpConnector() {
        Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
        connector.setScheme("http");
        connector.setPort(8080);
        connector.setSecure(false);
        connector.setRedirectPort(8443);
        return connector;
    }

}

客户端请求

Postman请求

  • 生成公钥public.key
keytool -list -rfc --keystore clientAuth_vmi_th.jks | openssl x509 -inform pem –pubkey

显示的内容拷贝到文件 public.key

  • 生成私钥private.key
keytool -v -importkeystore -srckeystore clientAuth_vmi_th.jks -srcstoretype jks -srcstorepass Pass1234 -destkeystore clientAuth_vmi_th.pfx -deststoretype pkcs12 -deststorepass Pass1234 -destkeypass Pass1234
openssl pkcs12 -in clientAuth_vmi_th.pfx -nocerts -nodes -out private.key
  • 添加证书
    https://xhope.top//wp-content/uploads/2022/07/1.png

Java 代码OkHttp请求

SSLTest.java

class SSLTest {
    private static final String KEYSTOREPASS = "Pass1234";
    private static final String KEYPASS = "Pass1234";

    static KeyStore readStore() throws Exception {
        try (InputStream keyStoreStream = new FileInputStream("/Users/rick/jkxyx205/ca/clientAuth_vmi_th.jks")) {
            KeyStore keyStore = KeyStore.getInstance("JKS");
            keyStore.load(keyStoreStream, KEYSTOREPASS.toCharArray());
            return keyStore;
        }
    }

    static SSLSocketFactory getSSLSocketFactory() {
        SSLSocketFactory factory = null;
        try {
            //初始化SSLContext
            SSLContext sslContext = SSLContexts.custom()
                    .loadKeyMaterial(readStore(), KEYPASS.toCharArray())
                    .build();

            factory = sslContext.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return factory;
    }

    public static void main(String[] args) throws IOException {
        OkHttpClient client = new OkHttpClient.Builder()
                .sslSocketFactory(SSLHelper2.getSSLSocketFactory())
                .readTimeout(30, TimeUnit.SECONDS)
                .writeTimeout(30, TimeUnit.SECONDS)
                .connectTimeout(6, TimeUnit.SECONDS)
                .hostnameVerifier(new NoopHostnameVerifier())
                .build();
        Request request = new Request.Builder()
                .url("https://test.com:8443/say-hello")
                .get()
                .build();

        Response response = client.newCall(request).execute();
        System.out.println(response.body().string()); // hello
    }
}

参考链接:
https://www.cnblogs.com/youngdeng/p/12868717.html

认证工具类

CertificateCoder.java

public class CertificateCoder {
    /**
     * Java密钥库(Java Key Store,JKS)KEY_STORE
     */
    public static final String KEY_STORE = "JKS";

    public static final String X509 = "X.509";

    /**
     * 由 KeyStore获得私钥
     *
     * @param keyStorePath
     * @param keyStorePassword
     * @param alias
     * @param aliasPassword
     * @return
     * @throws Exception
     */
    private static PrivateKey getPrivateKey(String keyStorePath,
                                            String keyStorePassword, String alias, String aliasPassword)
            throws Exception {
        KeyStore ks = getKeyStore(keyStorePath, keyStorePassword);
        PrivateKey key = (PrivateKey) ks.getKey(alias,
                aliasPassword.toCharArray());
        return key;
    }

    /**
     * 由 Certificate获得公钥
     *
     * @param certificatePath
     * @return
     * @throws Exception
     */
    private static PublicKey getPublicKey(String certificatePath)
            throws Exception {
        Certificate certificate = getCertificate(certificatePath);
        PublicKey key = certificate.getPublicKey();
        return key;
    }

    /**
     * 获得Certificate
     *
     * @param certificatePath
     * @return
     * @throws Exception
     */
    private static Certificate getCertificate(String certificatePath)
            throws Exception {
        CertificateFactory certificateFactory = CertificateFactory
                .getInstance(X509);
        FileInputStream in = new FileInputStream(certificatePath);

        Certificate certificate = certificateFactory.generateCertificate(in);
        in.close();

        return certificate;
    }

    /**
     * 获得Certificate
     *
     * @param keyStorePath
     * @param keyStorePassword
     * @param alias
     * @return
     * @throws Exception
     */
    private static Certificate getCertificate(String keyStorePath,
                                              String keyStorePassword, String alias) throws Exception {
        KeyStore ks = getKeyStore(keyStorePath, keyStorePassword);
        Certificate certificate = ks.getCertificate(alias);

        return certificate;
    }

    /**
     * 获得KeyStore
     *
     * @param keyStorePath
     * @param password
     * @return
     * @throws Exception
     */
    private static KeyStore getKeyStore(String keyStorePath, String password)
            throws Exception {
        FileInputStream is = new FileInputStream(keyStorePath);
        KeyStore ks = KeyStore.getInstance(KEY_STORE);
        ks.load(is, password.toCharArray());
        is.close();
        return ks;
    }

    /**
     * 私钥加密
     *
     * @param data
     * @param keyStorePath
     * @param keyStorePassword
     * @param alias
     * @param aliasPassword
     * @return
     * @throws Exception
     */
    public static byte[] encryptByPrivateKey(byte[] data, String keyStorePath,
                                             String keyStorePassword, String alias, String aliasPassword)
            throws Exception {
        // 取得私钥
        PrivateKey privateKey = getPrivateKey(keyStorePath, keyStorePassword,
                alias, aliasPassword);

        // 对数据加密
        Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);

        return cipher.doFinal(data);

    }

    /**
     * 私钥解密
     *
     * @param data
     * @param keyStorePath
     * @param alias
     * @param keyStorePassword
     * @param aliasPassword
     * @return
     * @throws Exception
     */
    public static byte[] decryptByPrivateKey(byte[] data, String keyStorePath,
                                             String alias, String keyStorePassword, String aliasPassword)
            throws Exception {
        // 取得私钥
        PrivateKey privateKey = getPrivateKey(keyStorePath, keyStorePassword,
                alias, aliasPassword);

        // 对数据加密
        Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
        cipher.init(Cipher.DECRYPT_MODE, privateKey);

        return cipher.doFinal(data);

    }

    /**
     * 公钥加密
     *
     * @param data
     * @param keyStorePath
     * @param alias
     * @param keyStorePassword
     * @return
     * @throws Exception
     */
    public static byte[] encryptByPublicKey(byte[] data, String keyStorePath,
                                            String alias, String keyStorePassword)
            throws Exception {

        // 取得公钥
        PublicKey publicKey = getCertificate(keyStorePath, keyStorePassword, alias).getPublicKey();
        // 对数据加密
        Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);

        return cipher.doFinal(data);

    }

    /**
     * 公钥加密
     *
     * @param data
     * @param certificatePath
     * @return
     * @throws Exception
     */
    public static byte[] encryptByPublicKey(byte[] data, String certificatePath)
            throws Exception {

        // 取得公钥
        PublicKey publicKey = getPublicKey(certificatePath);
        // 对数据加密
        Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);

        return cipher.doFinal(data);

    }

    /**
     * 公钥解密
     *
     * @param data
     * @param certificatePath
     * @return
     * @throws Exception
     */
    public static byte[] decryptByPublicKey(byte[] data, String certificatePath)
            throws Exception {
        // 取得公钥
        PublicKey publicKey = getPublicKey(certificatePath);

        // 对数据加密
        Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
        cipher.init(Cipher.DECRYPT_MODE, publicKey);

        return cipher.doFinal(data);

    }

    /**
     * 验证Certificate
     *
     * @param certificatePath
     * @return
     */
    public static boolean verifyCertificate(String certificatePath) {
        return verifyCertificate(new Date(), certificatePath);
    }

    /**
     * 验证Certificate是否过期或无效
     *
     * @param date
     * @param certificatePath
     * @return
     */
    public static boolean verifyCertificate(Date date, String certificatePath) {
        boolean status = true;
        try {
            // 取得证书
            Certificate certificate = getCertificate(certificatePath);
            // 验证证书是否过期或无效
            status = verifyCertificate(date, certificate);
        } catch (Exception e) {
            status = false;
        }
        return status;
    }

    /**
     * 验证证书是否过期或无效
     *
     * @param date
     * @param certificate
     * @return
     */
    private static boolean verifyCertificate(Date date, Certificate certificate) {
        boolean status = true;
        try {
            X509Certificate x509Certificate = (X509Certificate) certificate;
            x509Certificate.checkValidity(date);
        } catch (Exception e) {
            status = false;
        }
        return status;
    }

    /**
     * 签名
     *
     * @param keyStorePath
     * @param alias
     * @param keyStorePassword
     * @param aliasPassword
     * @return
     * @throws Exception
     */
    public static byte[] sign(byte[] sign, String keyStorePath, String alias,
                              String keyStorePassword, String aliasPassword) throws Exception {
        // 获得证书
        X509Certificate x509Certificate = (X509Certificate) getCertificate(
                keyStorePath, keyStorePassword, alias);

        // 取得私钥
        PrivateKey privateKey = getPrivateKey(keyStorePath, keyStorePassword,
                alias, aliasPassword);

        // 构建签名
        Signature signature = Signature.getInstance(x509Certificate
                .getSigAlgName());
        signature.initSign(privateKey);
        signature.update(sign);
        return signature.sign();
    }

    /**
     * 验证签名
     *
     * @param data
     * @param sign
     * @param certificatePath
     * @return
     * @throws Exception
     */
    public static boolean verify(byte[] data, byte[] sign,
                                 String certificatePath) throws Exception {
        // 获得证书
        X509Certificate x509Certificate = (X509Certificate) getCertificate(certificatePath);
        // 获得公钥
        PublicKey publicKey = x509Certificate.getPublicKey();
        // 构建签名
        Signature signature = Signature.getInstance(x509Certificate
                .getSigAlgName());
        signature.initVerify(publicKey);
        signature.update(data);

        return signature.verify(sign);

    }

    /**
     * 验证Certificate
     *
     * @param keyStorePath
     * @param keyStorePassword
     * @param alias
     * @return
     */
    public static boolean verifyCertificate(Date date, String keyStorePath,
                                            String keyStorePassword, String alias) {
        boolean status = true;
        try {
            Certificate certificate = getCertificate(keyStorePath,
                    keyStorePassword, alias);
            status = verifyCertificate(date, certificate);
        } catch (Exception e) {
            status = false;
        }
        return status;
    }

    /**
     * 验证Certificate
     *
     * @param keyStorePath
     * @param keyStorePassword
     * @param alias
     * @return
     */
    public static boolean verifyCertificate(String keyStorePath,
                                            String keyStorePassword, String alias) {
        return verifyCertificate(new Date(), keyStorePath, keyStorePassword,
                alias);
    }
}

参考链接:
https://xhope.top/?p=1336

理解

  • 一般情况,公钥加密,私钥解密。证书中包含公钥发送给客户端用于加密。
  • 密钥库中既有公钥又有私钥
  • 证书有很多格式,服务器不一样需要的格式也不一样。大体分为文本和二进制格式,是否保存私钥,是否需要密码。

其他参考链接

https://www.thomasvitale.com/https-spring-boot-ssl-certificate/
https://www.cnblogs.com/xa-xiaochen/p/15671213.html