问题描述
我正在尝试使用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提供了用于Web客户端的ssl配置示例的链接,但仍未回答如何加载CA证书,密钥和密码的问题。
如果您只想加载pem格式的证书(ca和自己的受信任证书),则可以使用jdk中可用的类。但是您还希望将密钥材料作为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,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,IOException {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null,null);
return keyStore;
}
public static void main(String[] args) throws PKCSException,OperatorCreationException,IOException,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,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,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();
}
}
它使用与我在第一个代码段中共享的内容相同的代码段。