說明
上篇詳細描述了自定義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雙向認證的代碼,我們安卓同事最終也是參考這個代碼做的實現。