说明
上篇详细描述了自定义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双向认证的代码,我们安卓同事最终也是参考这个代码做的实现。