近期收到一些小伙伴问 Java 中如何调用 https 中双向认证的接口的问题,本文采用 okhttp 以及 Java11 中的 httpclient 来介绍如何使用 Java 代码调用 https 双向认证的接口。
前置工作
- 生成服务端、客户端证书,并将各自的公钥加入到对方的信任链中以供后续的使用;
- 搭建一个 https 协议的服务以供客户端调用。
制作证书
这里我们采用 jdk 提供的 keytool 命令来生成我们所需要的服务端、客户端证书,并且可以使用 keytool 命令来将各自的公钥加入到对方的信任连中。
生成服务端、客户端密钥库以及证书
1 2 3 4
| # 生成服务端keystore,这里加拓展项的目的是为了说明域名验证的问题 keytool -genkey -validity 3650 -alias server -keyalg RSA -keystore server.keystore -ext san=dns:www.goku.edu,ip:127.0.0.1 # 生成客户端keystore,这里针对客户端的密钥库我们采用 PKCS12 的格式 keytool -genkey -validity 3650 -alias client -keyalg RSA -storetype PKCS12 -keystore client.p12
|
导出服务端、客户端证书中的公钥
1 2 3 4
| # 导出服务端证书的公钥 keytool -export -keystore server.keystore -alias server -file server.cer -rfc # 导出客户端证书的公钥 keytool -export -keystore client.p12 -storetype PKCS12 -alias client -file client.cer
|
将各自的公钥加入到对方的密钥库中
1 2 3 4
| # 将服务端证书导入到客户端的信任库中 keytool -import -alias server -file server.cer -keystore client.p12 # 将客户端证书导入到服务端的信任库中 keytool -import -alias client -file client.cer -keystore server.keystore
|
查看密钥库
可以通过如下命令,来查看公钥是否加入到各自的密钥库中
1 2
| keytool -list -v -keystore server.keystore keytool -list -rfc -keystore client.p12
|
搭建 HTTPS 服务
由于搭建服务不是本文的重点,建议使用 SpringBoot 脚手架快速搭建一个服务,下面只列关键点。
编写一个简单的 HTTP 接口
代码如下:
1 2 3 4 5 6 7 8 9
| @RestController @RequestMapping("/index") public class IndexRestController {
@GetMapping("/hello") public String hello() { return "Hello World!"; } }
|
完成之后,启动服务,使用浏览器打开对应的接口地址,如果正确输出Hello World!
则说明接口正常。
开启 SpringBoot 的 SSL 双向认证
在 application.properties
中新增如下配置,即可开启 SSL 的双向认证
1 2 3 4 5 6 7 8 9
| server.ssl.enabled=true server.ssl.protocol=TLS server.ssl.key-store=classpath:certs/server.keystore server.ssl.key-store-password=111111 server.ssl.key-password=111111 server.ssl.key-alias=server server.ssl.trust-store=classpath:certs/server.keystore server.ssl.trust-store-password=111111 server.ssl.client-auth=NEED
|
配完之后,启动服务,再次使用浏览器打开上一步的接口地址,发现接口并不能正常使用,并且浏览器会提示”此站点的连接不安全”,如下图所示
使用 OkHttp
构建 OkHttpClient
构建 SSL 的双向认证,有两个要点
- 创建 SSLSocket
- 创建 HostnameVerifier
创建 SSLSocketFactory
在进行 HTTPS 的请求时,最重要的就是创建 SSLSocket ,在 Java 中可以通过 SSLSocketFactory 来创建 SSLSocket,故在构建 OkHttpClient 时成员变量 SSLSocketFactory 的创建就显得非常关键。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| private static SSLSocketFactory sslSocketFactory() { try (InputStream inputStream = HttpsUtils.class.getResourceAsStream("/certs/client.p12")) { KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(inputStream, "111111".toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(keyStore, "111111".toCharArray());
String defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory tmf = TrustManagerFactory.getInstance(defaultAlgorithm); tmf.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
return sslContext.getSocketFactory(); } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | UnrecoverableKeyException | IOException | CertificateException e) { throw new RuntimeException(e); } }
|
对于第二个参数所需的 X509TrustManager,我们采用 OkHttp 提供的默认实现okhttp3.internal.Util#platformTrustManager
即可。
创建 HostnameVerifier
HostnameVerifier 是为了帮助我们验证我们请求的域名以及对应的 IP 是否一致。这里可以回顾一下创建证书时我们添加的拓展项-ext san=dns:www.goku.edu,ip:127.0.0.1
。由于我们在本机测试,可以信任对应的域名,也可以通过修改 hosts 文件的方式来指定域名,也可以自定义 HostnameVerifier 的实现。这里以不对域名做校验为例子,代码如下:
1 2 3
| private static HostnameVerifier hostnameVerifier() { return (hostname, session) -> true; }
|
在完成以上所有步骤之后,就可以构建我们的 OkHttpClient,并发送我们的 https 请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public static void main(String[] args) throws IOException { OkHttpClient httpClient = new OkHttpClient.Builder() .hostnameVerifier(hostnameVerifier()) .sslSocketFactory(sslSocketFactory(), Util.platformTrustManager()) .build(); Request request = new Request.Builder() .url("https://localhost:8443/index/hello") .build(); Response response = httpClient.newCall(request).execute(); System.out.println(response.body().string()); }
|
执行代码,发现控制台正确打印Hello World!
,说明我们对于 SSL 双向认证的 Https 请求已经发送成功,并成功的接收到响应了。
使用 Java11 的 HttpClient
熟悉 Java11 HttpClient 的开发者都知道 Java11 中的 HttpClient 跟 OkHttpClient 中的用法几乎一致。回归到 SSL 的双向认证,有以下两个不同点
- Java11 的 HttpClient 所需传的参数为 SSLContext,这个跟我们在使用 OkHttp 时创建 SSLSocketFactory 时初始化的 SSLContext 完全一致;
- Java11 的 HttpClient 并不支持自定义的 hostnameVerifier,只能通过配置项来关闭或打开域名验证。
其代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public static void main(String[] args) throws IOException, InterruptedException, KeyStoreException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException {
InputStream inputStream = HttpsUtils.class.getResourceAsStream("/certs/client.p12"); KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(inputStream, "111111".toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(keyStore, "111111".toCharArray());
String defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory tmf = TrustManagerFactory.getInstance(defaultAlgorithm); tmf.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
HttpClient httpClient = HttpClient.newBuilder() .sslContext(sslContext) .build();
HttpRequest request = HttpRequest.newBuilder() .GET() .uri(URI.create("https://www.goku.edu:8443/index/hello")) .header("Accept-Language", "zh-CN") .timeout(Duration.ofMillis(5000)) .build(); HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); }
|