我见过的大多数解决方案都是围绕使用keytool进行的,但都不符合我的情况。
这是一个非常简短的描述:我有一个PKCS12(.p12),它在禁用了证书验证的Postman中可以正常工作,但是通过编程,我总是最终收到服务器错误“ 400 Bad Request” /“没有发送必需的SSL证书” 。
原因是缺少TLS扩展SNI(服务器名称指示),下面是解决方法。
向SSL上下文添加扩展名/参数
在SSLContext初始化之后,添加以下内容:
SSLSocketFactory factory = sslContext.getSocketFactory();
try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
SSLParameters sslParameters = socket.getSSLParameters();
sslParameters.setServerNames(Collections.singletonList(new SNIHostName(hostName)));
socket.setSSLParameters(sslParameters);
socket.startHandshake();
}
在这种情况下的完整HTTP客户端类(不适用于生产)
注意1:SSLContextException和KeyStoreFactoryException只是扩展RuntimeException。
注意2:证书验证已禁用,此示例仅供开发人员使用。
注意3:在我的情况下,不需要禁用主机名验证,但我将其包括在注释行中
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import javax.net.ssl.*;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Objects;
public class SecureClientBuilder {
private String host;
private int port;
private boolean keyStoreProvided;
private String keyStorePath;
private String keyStorePassword;
public SecureClientBuilder withSocket(String host, int port) {
this.host = host;
this.port = port;
return this;
}
public SecureClientBuilder withKeystore(String keyStorePath, String keyStorePassword) {
this.keyStoreProvided = true;
this.keyStorePath = keyStorePath;
this.keyStorePassword = keyStorePassword;
return this;
}
public CloseableHttpClient build() {
SSLContext sslContext = keyStoreProvided
? getContextWithCertificate()
: SSLContexts.createDefault();
SSLConnectionSocketFactory sslSocketFactory =
new SSLConnectionSocketFactory(sslContext);
return HttpClients.custom()
.setSSLSocketFactory(sslSocketFactory)
.build();
}
private SSLContext getContextWithCertificate() {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(getKeyManagerFactory().getKeyManagers(), new TrustManager[]{getTrustManager()}, new SecureRandom());
SSLSocketFactory factory = sslContext.getSocketFactory();
try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
SSLParameters sslParameters = socket.getSSLParameters();
sslParameters.setServerNames(Collections.singletonList(new SNIHostName(host)));
socket.setSSLParameters(sslParameters);
socket.startHandshake();
}
return sslContext;
} catch (NoSuchAlgorithmException | KeyManagementException | IOException e) {
throw new SSLContextException("Could not create an SSL context with specified keystore.\nError: " + e.getMessage());
}
}
private KeyManagerFactory getKeyManagerFactory() {
try (FileInputStream fileInputStream = getResourceFile(keyStorePath)) {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(fileInputStream, keyStorePassword.toCharArray());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
return keyManagerFactory;
} catch (NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | IOException | KeyStoreException e) {
throw new KeyStoreFactoryException("Could not read the specified keystore.\nError: " + e.getMessage());
}
}
private TrustManager getTrustManager() {
return new X509TrustManager() {
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] arg0, String arg1) {
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] arg0, String arg1) {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
}
private FileInputStream getResourceFile(String keyStorePath) throws FileNotFoundException {
URL resourcePath = getClass().getClassLoader().getResource(keyStorePath);
return new FileInputStream(resourcePath.getFile());
}
}
使用上面的客户端构建器
注1:在“资源”文件夹中查找密钥库(.p12)。
注意2:设置标题“主机”以避免服务器错误“ 400-错误的请求”。
String hostname = "myHost";
int port = 443;
String keyStoreFile = "keystore.p12";
String keyStorePass = "somepassword";
String endpoint = String.format("https://%s:%d/endpoint", host, port);
CloseableHttpClient apacheClient = new SecureClientBuilder()
.withSocket(hostname, port)
.withKeystore(keyStoreFile, keyStorePass)
.build();
HttpGet get = new HttpGet(endpoint);
get.setHeader("Host", hostname + ":" + port);
CloseableHttpResponse httpResponse = apacheClient.execute(get);
assert httpResponse.getStatusLine().getStatusCode() == 200;
参考文件
https://docs.oracle.com/cn/java/javase/11/security/java-secure-socket-extension-jsse-reference-guide.html
CertificateRequest
消息询问时无法发送一个。造成此问题的其他原因可能包括没有指定服务器的发行者签名的证书,或者没有与指定服务器的密码匹配的证书。