客户端证书在建立连接时的 TLS 握手期间发送,不能通过该连接内的 HTTP 发送。
通信是这样分层的:
- HTTP(应用层协议)内
- TLS(表示层协议)内
- TCP(传输层协议)内
- IP(网络层协议)
您需要在 TLS 握手期间发送客户端证书,然后才能影响任何 HTTP(方法、标头、URL、请求正文)。服务器将不接受稍后发送的客户端证书。
我建议从DefaultHttpClient(已弃用)切换到CloseableHttpClient,它可以更干净地与 try-with-resources 一起使用。
Apache HttpClient 4.5使 Mutual TLS 相当方便。这个答案已经用Apache HttpClient 4.5.3进行了测试。
基本的起点是使用loadKeyMaterial将您的客户端证书及其密钥(客户端密钥对)加载到SSLContext 中:
SSLContext sslContext = SSLContexts.custom().loadKeyMaterial(
MutualHttpsMain.class.getResource(TEST_CLIENT_KEYSTORE_RESOURCE),
storePassword, keyPassword,
(aliases, socket) -> aliases.keySet().iterator().next()
).build();
最后用那个套接字工厂构建一个 HTTP 客户端:
CloseableHttpClient httpclient = HttpClients
.custom().setSSLContext(sslContext).build();
使用该客户端,您的所有请求都可以通过隐含的相互 TLS 身份验证来执行:
CloseableHttpResponse closeableHttpResponse = httpclient.execute(
new HttpGet(URI.create("https://mutual-tls.example.com/")));
这是使用 Apache HttpClient 的双向 TLS 的完整可运行示例:
import org.apache.http.HttpEntity;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import javax.net.ssl.SSLContext;
import java.io.Console;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.security.GeneralSecurityException;
public class MutualHttpsMain {
private static final String TEST_URL = "https://mutual-tls.example.com/";
private static final String TEST_CLIENT_KEYSTORE_RESOURCE = "/mutual-tls-keystore.p12";
public static void main(String[] args) throws GeneralSecurityException, IOException {
Console console = System.console();
char[] storePassword = console.readPassword("Key+Keystore password: ");
char[] keyPassword = storePassword;
SSLContext sslContext = SSLContexts.custom().loadKeyMaterial(
MutualHttpsMain.class.getResource(TEST_CLIENT_KEYSTORE_RESOURCE),
storePassword, keyPassword,
(aliases, socket) -> aliases.keySet().iterator().next()
).build();
try (CloseableHttpClient httpclient = HttpClients
.custom().setSSLContext(sslContext).build();
CloseableHttpResponse closeableHttpResponse = httpclient.execute(
new HttpGet(URI.create(TEST_URL)))) {
console.writer().println(closeableHttpResponse.getStatusLine());
HttpEntity entity = closeableHttpResponse.getEntity();
try (InputStream content = entity.getContent();
ReadableByteChannel src = Channels.newChannel(content);
WritableByteChannel dest = Channels.newChannel(System.out)) {
ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);
while (src.read(buffer) != -1) {
buffer.flip();
dest.write(buffer);
buffer.compact();
}
buffer.flip();
while (buffer.hasRemaining())
dest.write(buffer);
}
}
}
}
通常最好使用 Gradle 或 Maven 来运行这样的东西,但为了尽可能减少 Yak 剃须,我提供了用于构建和运行它的基线 JDK 指令。
从以下页面下载 JAR:
将上面的完整示例保存为MutualHttpsMain.java。
将您的 PKCS#12 复制到同一目录中的mutual-tls-keystore.p12。
编译如下(在 macOS/Linux/*nix-likes 上):
javac MutualHttpsMain.java -cp httpclient-4.5.3.jar:httpcore-4.4.8.jar
或在 Windows 上:
javac MutualHttpsMain.java -cp httpclient-4.5.3.jar;httpcore-4.4.8.jar
运行如下(在 macOS/Linux/*nix-likes 上):
java -cp httpclient-4.5.3.jar:commons-codec-1.10.jar:commons-logging-1.2.jar:httpcore-4.4.8.jar:. MutualHttpsMain
运行如下(在 Windows 上):
java -cp httpclient-4.5.3.jar;commons-codec-1.10.jar;commons-logging-1.2.jar;httpcore-4.4.8.jar;. MutualHttpsMain