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雙向認證的代碼,我們安卓同事最終也是參考這個代碼做的實現。

版權所有:塗有薄技

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