okhttp 2way tls
okhttp双向认证
双向认证和普通https的连接的主要区别:
- 需要提前分享客户端的证书到服务器
- 在建立连接的时候需要发送自己证书的信息
- 服务器会使用客户端证书对客户端证书验证
主要流程
- 加载客户端证书的证书和私钥到KeyStore
- 加载服务端证书到TrustStore
- 使用KeyStore和TrustStore创建SSLContext
- 使用SSLContext创建okhttp连接
示例代码
public class App
{
public static void main(String[] args)
{
try {
KeyManager[] keyStoreManager = loadKeyStore();
TrustManager[] trustManager = loadTrustStore();
SSLSocketFactory socketFactory = getSslSocketFactory(keyStoreManager, trustManager);
OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(socketFactory, (X509TrustManager) trustManager[0])
.hostnameVerifier((hostname, session) -> true)
.build();
Call call = client.newCall(new Request.Builder()
.url("https://2way.example.com")
.build());
Response response = call.execute();
if (response.isSuccessful()) {
System.out.printf(response.body().string());
}
} catch (Exception e) {
System.out.printf(e.toString());
}
}
private static KeyManager[] loadKeyStore() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException, UnrecoverableKeyException, InvalidKeySpecException {
// 这里是采用从原始crt和pem中加载证书和密钥,根据情况也很容易改从jks中加载
// create keyStore from client privateKey and cert
KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
// load client cert from file
// 提前准备client.crt文件在resource目录
Certificate clientCert = getCertFromFile("/client.crt");
clientKeyStore.load(null);
// 提前准备client.key.pem文件在resource目录
clientKeyStore.setKeyEntry("client-cert", readPrivateKeyFromPem("/client.key.pem"), null, new Certificate[] { clientCert } );
// crate KeyManger from keyStore
KeyManagerFactory keyManagerFactory_Client = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory_Client.init(clientKeyStore, null);
return keyManagerFactory_Client.getKeyManagers();
}
private static TrustManager[] loadTrustStore() throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException {
// load trust store from file
// 提前准备truststore.jks文件在resource目录,jks需要密码保护
KeyStore trustStore_Client = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream inputStreamTruststore = App.class.getResourceAsStream("/truststore.jks")) {
trustStore_Client.load(inputStreamTruststore, "<password_here>".toCharArray());
}
// TrustManagerFactory from keyStore
TrustManagerFactory trustManagerFactory_Client = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory_Client.init(trustStore_Client);
return trustManagerFactory_Client.getTrustManagers();
}
private static SSLSocketFactory getSslSocketFactory(KeyManager[] keyManagers_Client, TrustManager[] trustManagers_Client) throws NoSuchAlgorithmException, KeyManagementException {
SSLContext sslContext_Client = SSLContext.getInstance("TLSv1.2");
sslContext_Client.init(keyManagers_Client, trustManagers_Client, new SecureRandom());
return sslContext_Client.getSocketFactory();
}
private static Certificate getCertFromFile(String file) throws CertificateException {
InputStream inputStream = App.class.getResourceAsStream(file);
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Certificate certificate = certificateFactory.generateCertificate(inputStream);
return certificate;
}
private static PrivateKey readPrivateKeyFromPem(String file) throws NoSuchAlgorithmException, InvalidKeySpecException {
String key = new BufferedReader(
new InputStreamReader(App.class.getResourceAsStream(file), StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining(System.lineSeparator()));
String privateKeyPEM = key
.replace("-----BEGIN PRIVATE KEY-----", "")
.replaceAll(System.lineSeparator(), "")
.replace("-----END PRIVATE KEY-----", "");
byte[] encoded = Base64.getDecoder().decode(privateKeyPEM);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
return keyFactory.generatePrivate(keySpec);
}
}
主要步骤还是很明显,但是涉及到客户端证书,密钥和服务器证书都需要提前准备好。跟据相关文件的存放方式,加载的方法也要对应的做出修改。
测试
okhttp同提供了MockWebServer来帮助编写单元测试。
主要步骤:
- 服务器端证书,密钥和客户端证书
- 使用相关证书、密钥创建mockwebserver
- 客户端加载客户端证书,密钥和服务器端证书
- 客户端使用server.url()生成的连接访问mockwebserver
测试会用到大量的自签证书,如果不熟悉的话请先看第 准备自签证书 章节
// server,加载服务器端证书
Certificate serverCert = getCertFromFile("/server.crt");
KeyStore serverKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
serverKeyStore.load(null);
// 加载服务器端密钥
serverKeyStore.setKeyEntry("server-cert", readPrivateKeyFromPem("/server.key.pem"), null, new Certificate[] { serverCert } );
// crate KeyManger from keyStore
KeyManagerFactory keyManagerFactory_Server = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory_Server.init(serverKeyStore, null);
KeyManager[] keyManagers_Server = keyManagerFactory_Server.getKeyManagers();
//加载客户端证书
Certificate clientCaCert = getCertFromFile("/client.crt");
KeyStore trustKeyStore_Server = KeyStore.getInstance(KeyStore.getDefaultType());
trustKeyStore_Server.load(null);
trustKeyStore_Server.setCertificateEntry("client-cert", clientCaCert);
TrustManagerFactory trustManagerFactory_Server = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory_Server.init(trustKeyStore_Server);
TrustManager[] trustManagers_Server = trustManagerFactory_Server.getTrustManagers();
SSLContext sslContext_Server = SSLContext.getInstance("TLSv1.2");
sslContext_Server.init(keyManagers_Server, trustManagers_Server, new SecureRandom());
//创建MockWebServer
MockWebServer server = new MockWebServer();
//加载sslcontext
server.useHttps(sslContext_Server.getSocketFactory(), false);
server.requireClientAuth();
server.enqueue(new MockResponse());
// make call
// 使用server.url来替换要访问的url
Call call = client.newCall(new Request.Builder()
.url(server.url("/"))
.build());
Response response = call.execute();
//查看证书信息
System.out.println(response.handshake().peerPrincipal());
RecordedRequest recordedRequest = server.takeRequest();
System.out.println(recordedRequest.getHandshake().peerPrincipal());
准备自签证书
TODO openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365