如何在 Java 中使用 Webclient 添加 CA 证书和客户端证书

How to add both a CA certificate and a Client Certificate using Webclient in Java

我正在尝试在 Java 中使用 Webclient 编写 API 调用。目前我在查找有关如何将证书添加到 Webclient 的文档时遇到问题。我想提供一个 PEM 格式的 CA 证书文件,以及一个客户端证书,我将在其中提供一个主机、一个 CRT 文件、一个密钥文件和一个密码。我已经在邮递员中使用了此设置,但我想将其转移到 Java 应用程序。下面是我的代码。

       Gson gson = new Gson();
       LinkedHashMap<String, Object> reqBody
               = new LinkedHashMap<String, Object>();
       LinkedHashMap<String, String> variables
               = new LinkedHashMap<String, String>();
       reqBody.put("variables", variables);



       WebClient webClient = WebClient.builder()
               .baseUrl("sampleurl.com")
               .defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
               .defaultHeader(HttpHeaders.ACCEPT, "application/json")
               .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json")
               .build();


       return webClient.post()
               .uri("/api")
               .headers(headers -> headers.setBasicAuth("userName", "password"))
               .body(Mono.just(reqBody), LinkedHashMap.class)//if directly putting the map doesn't work
               //can also convert to json string then to monoflux
               .retrieve()
               .bodyToMono(String.class);

尽管 michalk 提供了一个 link 用于 web 客户端的示例 ssl 配置,但关于如何加载 CA 证书、密钥和密码的问题仍然没有得到解答。

如果您只想加载 pem 格式的(ca 和自己的可信证书)证书,您可以使用 jdk 中可用的 类。但是您还想将密钥 material 作为 pem 文件加载,而这对于 jdk 中的默认 类 是不可能的。对于这个用例,我会推荐 Bouncy castle:

maven 依赖:

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15on</artifactId>
</dependency>
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

class App {

    private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider();
    private static final JcaPEMKeyConverter KEY_CONVERTER = new JcaPEMKeyConverter().setProvider(BOUNCY_CASTLE_PROVIDER);
    private static final String CERTIFICATE_TYPE = "X.509";
    private static final Pattern CERTIFICATE_PATTERN = Pattern.compile("-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----", Pattern.DOTALL);
    private static final String NEW_LINE = "\n";
    private static final String EMPTY = "";

    public static PrivateKey parsePrivateKey(String privateKeyContent, char[] keyPassword) throws IOException, PKCSException, OperatorCreationException {
        PEMParser pemParser = new PEMParser(new StringReader(privateKeyContent));
        PrivateKeyInfo privateKeyInfo = null;

        Object object = pemParser.readObject();

        while (object != null && privateKeyInfo == null) {
            if (object instanceof PrivateKeyInfo) {
                privateKeyInfo = (PrivateKeyInfo) object;
            } else if (object instanceof PEMKeyPair) {
                privateKeyInfo = ((PEMKeyPair) object).getPrivateKeyInfo();
            } else if (object instanceof PKCS8EncryptedPrivateKeyInfo) {
                InputDecryptorProvider inputDecryptorProvider = new JceOpenSSLPKCS8DecryptorProviderBuilder()
                        .setProvider(BOUNCY_CASTLE_PROVIDER)
                        .build(Objects.requireNonNull(keyPassword));

                privateKeyInfo = ((PKCS8EncryptedPrivateKeyInfo) object).decryptPrivateKeyInfo(inputDecryptorProvider);
            } else if (object instanceof PEMEncryptedKeyPair) {
                PEMDecryptorProvider pemDecryptorProvider = new JcePEMDecryptorProviderBuilder()
                        .setProvider(BOUNCY_CASTLE_PROVIDER)
                        .build(keyPassword);

                PEMKeyPair pemKeyPair = ((PEMEncryptedKeyPair) object).decryptKeyPair(pemDecryptorProvider);
                privateKeyInfo = pemKeyPair.getPrivateKeyInfo();
            }

            if (privateKeyInfo == null) {
                object = pemParser.readObject();
            }
        }

        if (Objects.isNull(privateKeyInfo)) {
            throw new IllegalArgumentException("Received an unsupported private key type");
        }

        return KEY_CONVERTER.getPrivateKey(privateKeyInfo);
    }

    public static List<Certificate> parseCertificate(String certificateContent) throws IOException, CertificateException {
        List<Certificate> certificates = new ArrayList<>();
        Matcher certificateMatcher = CERTIFICATE_PATTERN.matcher(certificateContent);

        while (certificateMatcher.find()) {
            String sanitizedCertificate = certificateMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
            byte[] decodedCertificate = Base64.getDecoder().decode(sanitizedCertificate);
            try(ByteArrayInputStream certificateAsInputStream = new ByteArrayInputStream(decodedCertificate)) {
                CertificateFactory certificateFactory = CertificateFactory.getInstance(CERTIFICATE_TYPE);
                Certificate certificate = certificateFactory.generateCertificate(certificateAsInputStream);
                certificates.add(certificate);
            }
        }

        return certificates;
    }

    public static <T extends Certificate> KeyStore createTrustStore(List<T> certificates) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException {
        KeyStore trustStore = createEmptyKeyStore();
        for (T certificate : certificates) {
            trustStore.setCertificateEntry(UUID.randomUUID().toString(), certificate);
        }
        return trustStore;
    }

    public static <T extends Certificate> KeyStore createKeyStore(PrivateKey privateKey, char[] privateKeyPassword, List<T> certificateChain) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException {
        KeyStore keyStore = createEmptyKeyStore();
        keyStore.setKeyEntry(UUID.randomUUID().toString(), privateKey, privateKeyPassword, certificateChain.toArray(new Certificate[0]));
        return keyStore;
    }

    public static KeyStore createEmptyKeyStore() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, null);
        return keyStore;
    }

    public static void main(String[] args) throws PKCSException, OperatorCreationException, IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
        String certificateChainContent = "";
        String privateKeyContent = "";
        char[] privateKeyPassword = "secret".toCharArray();

        String caCertificateContent = "";

        PrivateKey privateKey = parsePrivateKey(privateKeyContent, privateKeyPassword);
        List<Certificate> certificates = parseCertificate(certificateChainContent);

        List<Certificate> caCertificates = parseCertificate(caCertificateContent);

        KeyStore keyStore = createKeyStore(privateKey, privateKeyPassword, certificates);
        KeyStore trustStore = createTrustStore(caCertificates);

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, null);

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(trustStore);

        SslContext sslContext = SslContextBuilder.forClient()
                .keyManager(keyManagerFactory)
                .trustManager(trustManagerFactory)
                .build();

        HttpClient httpClient = HttpClient.create()
                .secure(sslSpec -> sslSpec.sslContext(sslContext));

        WebClient webClient = WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }

}

你能试试这个并分享结果吗?

我知道上面的代码片段有点冗长,所以如果你不想包含所有代码,我也可以为你提供一个可以实现相同目的的替代方案:

<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>sslcontext-kickstart-for-pem</artifactId>
    <version>5.3.0</version>
</dependency>
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import nl.altindag.sslcontext.util.PemUtils;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCSException;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;

import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509ExtendedTrustManager;
import java.io.IOException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;

class App {
    
    
    public static void main(String[] args) throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException, OperatorCreationException, PKCSException {
        X509ExtendedKeyManager keyManager = PemUtils.loadIdentityMaterial("certificateChain.pem", "private-key.pem", "secret".toCharArray());
        X509ExtendedTrustManager trustManager = PemUtils.loadTrustMaterial("ca-certificates.pem");

        SslContext sslContext = SslContextBuilder.forClient()
                .keyManager(keyManager)
                .trustManager(trustManager)
                .build();

        HttpClient httpClient = HttpClient.create()
                .secure(sslSpec -> sslSpec.sslContext(sslContext));

        WebClient webClient = WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }
}

它使用我在第一个代码片段中共享的相同代码片段。