CA双向认证补充:java客户端使用优化及证书链和Android证书

说明

上篇详细描述了自定义ca证书的步骤以及浏览器作为客户端和java作为客户端的使用方法。
但是之前的java客户端使用代码还存在一定的问题:
首先,之前的客户端根证书是在代码外部使用keytool安装到jdk证书库,次数多了就显得麻烦;
其次,之前的代码只能支持域名访问,这样没有真实域名时就必须更改host文件;
于是通过查找网络资料修改之后,便有了新的操作方式,使得java客户端可以支持ip访问https,同时不用直接侵入jdk。

支持ip访问https的java客户端

http请求有多种框架,java常用的可能就是httpclient,而目前公司安卓那边使用的是okhttp,因此除了尝试我所熟悉的httpclient之外,也做了okhttp在java环境下的尝试。

httpclient方式代码

以下代码基本来自网络,由于搜索资料过多,已经无法找到原出处。

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
/**
 * @Description:TODO类描述
 * @since JDK 1.8
 * @author tuzongxun
 * @Email [email protected]
 * @version: v1.0.0
 * @date: 2019年4月9日 下午2:43:35
 */
public class CaTest {
    // private String serverUrl = "https://blog.tzx.cn";
    private String serverUrl = "https://192.168.0.205";
    private SSLSocketFactory sslFactory = null;
    /**
     * @author: tuzongxun
     * @date: 2019年4月9日 下午2:42:03
*/ public void run() { try { HttpURLConnection connection = doHttpRequest(serverUrl, "GET", "", null); int responseCode = getResponseCode(connection); String responseBody = getResponseBodyAsString(connection); connection.disconnect(); System.out.println("response code=" + responseCode); System.out.println(responseBody); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { CaTest test = new CaTest(); test.run(); } private synchronized SSLSocketFactory getSSLFactory() throws Exception { if (sslFactory == null) { SSLContext sslContext = SSLContext.getInstance("SSL"); TrustManager[] tm = {new MyX509TrustManager()}; KeyStore truststore = KeyStore.getInstance("JKS"); //这里加载的是客户端证书 truststore.load(new FileInputStream("C:\\Users\\tzx\\Desktop\\client.jks"), "12345678" .toCharArray()); //上边的密码是生成jks时的密码 KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); //这个密码是证书密码 kmf.init(truststore, "12345678".toCharArray()); sslContext.init(kmf.getKeyManagers(), tm, new java.security.SecureRandom()); sslFactory = sslContext.getSocketFactory(); } return sslFactory; } private HttpURLConnection doHttpRequest(String requestUrl, String method, String body, Map<String, String> header) throws Exception { HttpURLConnection conn; if (method == null || method.length() == 0) { method = "GET"; } if ("GET".equals(method) && body != null && !body.isEmpty()) { requestUrl = requestUrl + "?" + body; } URL url = new URL(requestUrl); // start这一段代码必须加在open之前,即支持ip访问的关键代码 javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier(new javax.net.ssl.HostnameVerifier() { public boolean verify(String hostname, SSLSession sslsession) { return true; } }); //end conn = (HttpURLConnection)url.openConnection(); conn.setDoOutput(true); conn.setDoInput(true); conn.setUseCaches(false); conn.setInstanceFollowRedirects(true); conn.setRequestMethod(method); if (requestUrl.matches("^(https?)://.*$")) { ((HttpsURLConnection)conn).setSSLSocketFactory(this.getSSLFactory()); } if (header != null) { for (String key : header.keySet()) { conn.setRequestProperty(key, header.get(key)); } } if (body != null && !body.isEmpty()) { if (!method.equals("GET")) { OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); wr.write(body); wr.close(); } } conn.connect(); return conn; } public int getResponseCode(HttpURLConnection connection) throws IOException { return connection.getResponseCode(); } public String getResponseBodyAsString(HttpURLConnection connection) throws Exception { BufferedReader reader = null; if (connection.getResponseCode() == 200) { reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); } else { reader = new BufferedReader(new InputStreamReader(connection.getErrorStream())); } StringBuffer buffer = new StringBuffer(); String line = null; while ((line = reader.readLine()) != null) { buffer.append(line); } return buffer.toString(); } class MyX509TrustManager implements X509TrustManager { private X509TrustManager sunJSSEX509TrustManager; MyX509TrustManager() throws Exception { // create a "default" JSSE X509TrustManager. KeyStore ks = KeyStore.getInstance("JKS"); //这里加载的是根证书链 ks.load(new FileInputStream("C:\\Users\\tzx\\Desktop\\caroot.jks"), "12345678".toCharArray()); TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509", "SunJSSE"); tmf.init(ks); TrustManager tms[] = tmf.getTrustManagers(); for (int i = 0; i < tms.length; i++) { if (tms[i] instanceof X509TrustManager) { sunJSSEX509TrustManager = (X509TrustManager)tms[i]; return; } } throw new Exception("Couldn't initialize"); } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { try { sunJSSEX509TrustManager.checkClientTrusted(chain, authType); } catch (CertificateException excep) { } } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { try { sunJSSEX509TrustManager.checkServerTrusted(chain, authType); } catch (CertificateException excep) { } } @Override public X509Certificate[] getAcceptedIssuers() { return sunJSSEX509TrustManager.getAcceptedIssuers(); } } }

okhttp代码

okhttp使用时和httpclient基本一样,区别只是run方法里的httpclient替换一下:

OkHttpClient client = new OkHttpClient.Builder().sslSocketFactory(this.getSSLFactory()).build();
Request request = new Request.Builder().url(serverUrl).build();
Call call = client.newCall(request);
Response response = call.execute();
System.out.println(response);
System.out.println(response.body().string().toString());
response.close();

使用时要注意导入okhttp的依赖,例如:

<dependency>
  	<groupId>com.squareup.okhttp3</groupId>
  	<artifactId>okhttp</artifactId>
  	<version>3.3.0</version>
</dependency>

客户端通信证书jks文件生成

上一篇说了如何生成p12文件,p12实际就是pkcs12格式的文件,我的理解就是一个压缩包,里边包含了crt证书和key文件。
而pkcs12格式的这种文件有时候也会命名为pfx,可以用命令转换为jks格式:

keytool -importkeystore -srckeystore  client.pfx -srcstoretype pkcs12 -destkeystore client.jks -deststoretype JKS

客户端根证书链jks文件生成

上一遍中使用的根证书只包含单一的一个crt文件,这种证书称作单证书,而实际项目中为了更加安全,则很可能使用根证书链。
据我个人理解,根证书链大概的意思就是,首先如上一篇说的那样生成一个顶级根证书,然后再由这个根证书签发下级根证书,甚至再下一级的根证书。
最终校验的时候是根据整个证书链进行校验,而不是单证书校验,从而进一步增加安全性。
根证书链一般可能会以pem作为文件结尾,文件里包含了层层根证书以及对应关系,pem根证书链也可以生成jks格式的证书链:

keytool -import -noprompt -file caroot.pem -keystore caroot.jks -storepass 11111111

上述操作我理解为也是一个打包的过程,需要指定一个打包密码。

那么上边两个生成的文件即我们java客户端双向认证需要的两个jks证书文件了。

Android证书文件

由于我们这一次的双向认证实际需求并不是java客户端,而是安卓,而据说安卓里不识别jks、pfx等证书,因此只能把证书转换为安卓可以使用的。
经过多次调试,最终有一个可行的方案是,安卓根证书使用cer格式的根证书链文件,安卓通信证书则使用bks格式的证书文件。
因此实际操作也就是把上百年的pem根证书链转换为cer文件;把上边的client.jks转换为bks文件,cer转换可以使用如下命令:

openssl x509 -inform pem -in caroot.pem -outform der -out caroot.cer

而jks转换为bks似乎并不能直接用命令,最终是参考网上的文章借助一个工具进行转换,参考文章:
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0831/3393.html
上边的文章不仅包含了bks的转换,也包含了安卓端okhttp双向认证的代码,我们安卓同事最终也是参考这个代码做的实现。

版权所有:涂有薄技

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章