Android实现https双向认证实践总结

这篇文章主要总结了自己对https的一些浅显的理解,以及如何在Android开发中实现双向认证。
开发环境:ubuntu 16.04

1.1 https的原理

关于https的工作原理我强烈推荐你看一下郭霖的博客「写一篇最好懂的HTTPS讲解」。 大神不愧是大神,之前自己也看了许多篇讲解https基本原理和基本流程的文章,但讲的都不是很清晰,看了他的这篇文章之后豁然开朗,感觉对对于https工作的流程一下子清晰了许多。这部分知识有时间了自己需要专门整理一下。

1.2 自签名证书部署服务,实践双向认证

自己首先参照鸿洋的博客Android Https相关完全解析 当OkHttp遇到Https(鸿洋) 下载了tomcat服务器

1.2.1 实现单项认证

在实现双向认证之前我们先实践一下单项认证。服务器上放一个证书(私钥),Android客户端放一个证书(公钥),服务器只对客户端的证书做校验,这就是单项认证。
我们需要三步:

  • 安装tomcat服务器
  • 生成证书
  • 并配置服务器
  • Android客户端代码的编写

1 安装tomcat服务器

这一步自己是参照博客Ubuntu 16.04安装tomcat8来进行安装的。我的笔记本安装的ubuntu16.04系统。

首先点击这里打开下载链接,点击下图红框部分开始下载安装包。

35-2-tomcat下载链接

打开下载目录,右键打开命令行,依次执行下方命令进行安装

1
2
3
4
5
6
7
8
//解压
tar zxvf apache-tomcat-8.5.24.tar.gz
//将解压后的文件夹移动到opt目录中
sudo mv apache-tomcat-8.5.24/ /opt/apache-tomcat-8.5.24
//创立连接
sudo ln -s /opt/apache-tomcat-8.5.24/ /opt/tomcat8
//启动服务
/opt/tomcat8/bin/startup.sh

启动tomcat服务后,打开链接http://127.0.0.1:8080/进行测试, 页面出现400错误,根据博客ubuntu下tomcat显示启动成功但是页面400错误中说是因为8080端口被占用了,所以自己将端口修改为8000,然后重新刷新页面,成功打开tomcat的首页。

进入目录/opt/apache-tomcat-8.5.53/conf, 输入命令sudo vim server.xml打开server.xml,然后将端口号修改为8000,代码如下。

1
2
3
<Connector port="8000" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />

输入如下命令重启服务

1
2
3
4
//关闭服务
/opt/tomcat8/bin/shutdown.sh
//启动服务
/opt/tomcat8/bin/startup.sh

再次输入链接http://127.0.0.1:8080/就可以看到tomcat的首页了,到这里tomcat就算安装完成了,如图35-3。

35-3-tomcat首页

2 生成证书,并配置服务

使用https首先需要一个证书,使用下方命令生成一个证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
keytool -genkey -alias syc_server -keyalg RSA -keystore syc_server.jks -validity 3600 -storepass 123456
您的名字与姓氏是什么?
[Unknown]: syc
您的组织单位名称是什么?
[Unknown]: syc
您的组织名称是什么?
[Unknown]: syc
您所在的城市或区域名称是什么?
[Unknown]: henan
您所在的省/市/自治区名称是什么?
[Unknown]: henan
该单位的双字母国家/地区代码是什么?
[Unknown]: cn
CN=zhang, OU=zhang, O=zhang, L=xian, ST=shanxi, C=cn是否正确?
[否]: y

输入 <zhy_server> 的密钥口令
(如果和密钥库口令相同, 按回车):

细心观察,可以发现上面命令中有一个关键字RSA,可以知道这个证书使用的是RSA算法,这和郭霖那篇博客中说的一样, https开始握手的时候一般采用的就是RSA,也就是非对称加密。 执行完命令,当前目录中会生成一个证书文件syc_server.jks,密钥的口令就是命令中输入的123456,这个jks证书放在服务器上,也就是私钥,下面我们生成一个公钥证书。

输入下方命令利用syc_server.jks生成(也可以称作导出)一个证书

1
keytool -export -alias syc_server -file syc_server.cer -keystore syc_server.jks -storepass 123456

执行完命令后可以发现当前目录多了一个syc_server文件,这个证书就是公钥,是要放在Android客户端的。至此,生成证书这一步就算完成了。

3 配置服务

syc_server.jks放入到/home/shaoyance/temp/(填写你的路径)目录中。

输入命令sudo vim server.xml打开server.xml, 在Service标签中添加以下代码

1
2
3
4
5
6
<Connector SSLEnabled="true" acceptCount="100" clientAuth="false"
disableUploadTimeout="true" enableLookups="true" keystoreFile="/home/shaoyance/temp/syc_server.jks" keystorePass="123456" maxSpareThreads="75"
maxThreads="200" minSpareThreads="5" port="8443"
protocol="org.apache.coyote.http11.Http11NioProtocol" scheme="https"
secure="true" sslProtocol="TLS"
/>

上边代码中keystoreFile表示服务器证书(私钥)的路径, keystorePass为证书的口令。 修改了配置文件后输入下方命令重启服务

1
2
/opt/tomcat8/bin/shutdown.sh
/opt/tomcat8/bin/startup.sh

注意如果服务器在关闭的状态下我们输入关闭的命令/opt/tomcat8/bin/shutdown.sh会报如下的错误,这个不用管。

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
 /opt/tomcat8/bin/shutdown.sh
Using CATALINA_BASE: /opt/tomcat8
Using CATALINA_HOME: /opt/tomcat8
Using CATALINA_TMPDIR: /opt/tomcat8/temp
Using JRE_HOME: /usr
Using CLASSPATH: /opt/tomcat8/bin/bootstrap.jar:/opt/tomcat8/bin/tomcat-juli.jar
四月 20, 2020 1:00:52 上午 org.apache.catalina.startup.Catalina stopServer
严重: Could not contact [localhost:8005]. Tomcat may not be running.
四月 20, 2020 1:00:52 上午 org.apache.catalina.startup.Catalina stopServer
严重: Catalina.stop:
java.net.ConnectException: 拒绝连接 (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at java.net.Socket.connect(Socket.java:538)
at java.net.Socket.<init>(Socket.java:434)
at java.net.Socket.<init>(Socket.java:211)
at org.apache.catalina.startup.Catalina.stopServer(Catalina.java:504)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.catalina.startup.Bootstrap.stopServer(Bootstrap.java:389)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:479)

启动成功后输入https://localhost:8443/打开网页,发现提示证书不信任,选择添加例外,接受风险并继续,然后进入网页即可打开tomcat的主页,如图35-4-1和35-4。

35-4-1-tomcat-https 接受风险并继续

35-4-tomcat-https 首页

4. Android客户端代码的编写

因为自己不太会写后台代码, 这里我直接使用的tomcat默认的页面进行测试。

创建一个项目DemoAndroidLearn,将公钥证书syc_server.cer放到项目的assets目录中,也可以使用如下命令获取证书中的字符串,如图35-5.

1
keytool -printcert -rfc -file syc_server.cer

35-5-使用命令行获取证书字符串

放在android中的这个证书就是一个公钥,你可能会有这样的疑问,如果别人将apk解压然后破解了公钥这样不是不安全了吗,其实没有关系的,因为证书采用了非对称加密rsa的算法, 他们只有公钥而没有服务器的那个私钥证书,仍然是无法获取请求的内容的。

然后在Application的onCreat中编写如下代码(自己使用的是鸿洋的okhttputils ,retrofit设置https和这个原理差不多,底层都使用了okhttp)。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
    private static final String InterceptTag = "OkHttp";
private static final String SERVER_CER ="syc_server.cer" ;
private static final String CLIENT_JKS ="client.bks" ;
String[] VERIFY_HOST_NAME_ARRAY = new String[]{"192.168.8.102"};
@Override
public void onCreate() {
ClearableCookieJar cookieJar1 = new PersistentCookieJar(new SetCookieCache(), new SharedPrefsCookiePersistor(this));
InputStream severInputStream = null;
InputStream clientInputStream = null;
try {
severInputStream = getApplicationContext().getAssets().open(SERVER_CER);
// clientInputStream = getApplicationContext().getAssets().open(CLIENT_JKS);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (severInputStream != null) {
FileUtil.safeCloseIs(severInputStream);
FileUtil.safeCloseIs(clientInputStream);
}
}
LoggerInterceptor interceptor = new LoggerInterceptor(InterceptTag);
// StethoInterceptor interceptor = new StethoInterceptor();
// HttpsUtils.SSLParams sslParams = HttpsUtils.getSslSocketFactory(null, null, null);
HttpsUtils.SSLParams sslParams = HttpsUtils.getSslSocketFactory(new InputStream[]{severInputStream}, null, null);
// HttpsUtils.SSLParams sslParams = HttpsUtils.getSslSocketFactory(new InputStream[]{severInputStream}, clientInputStream, "111111");
OkHttpClient okHttpClient = new OkHttpClient.Builder()
// .proxySelector(new ProxySelector() {
// @Override
// public List<Proxy> select(URI uri) {
// //禁止代理
// return Collections.singletonList(Proxy.NO_PROXY);
// }
//
// @Override
// public void connectFailed(URI uri, SocketAddress socketAddress, IOException e) {
//
// }
// })
.connectTimeout(10000L, TimeUnit.MILLISECONDS)
.readTimeout(10000L, TimeUnit.MILLISECONDS)
.writeTimeout(10000L, TimeUnit.MILLISECONDS)
.cookieJar(cookieJar1)
.addNetworkInterceptor(interceptor)
.sslSocketFactory(sslParams.sSLSocketFactory, sslParams.trustManager)
.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
if (TextUtils.isEmpty(hostname)) {
return false;
}
// return true;
return Arrays.asList(VERIFY_HOST_NAME_ARRAY).contains(hostname);

}
})
.build();

OkHttpUtils.initClient(okHttpClient);
}

上边代码中SERVER_CER就是我们放在assets目录中的证书, CLIENT_JKS这个双向认证会用到。

192.168.8.102是自己电脑在局域网中的ip,此处也就是服务器的ip。 然后我们通过下边代码设置本证书校验

1
2
3
//参数一: 就是本地公钥证书对应的流
//参数二和参数三在下边双向认证中会用到,这里我们设置为null
HttpsUtils.SSLParams sslParams = HttpsUtils.getSslSocketFactory(new InputStream[]{severInputStream}, null, null);

proxySelector中的Collections.singletonList(Proxy.NO_PROXY)表示禁止使用代理,设置了这行代码,别人就无法使用fiddler抓包工具进行抓包了。 hostnameVerifier中的verify方法对服务器主机名的合法性进行校验,可以防止中间人攻击。可以参考博客Java的HostnameVerifier

创建一个页面ActivityNetWork中编写如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String url = "https://192.168.8.102:8443/";
OkHttpUtils
.get()
.url(url)
.build()
.execute(new StringCallback() {
@Override
public void onError(Call call, Exception e, int id) {
LogUtils.e(TAG, e.getMessage());
}

@Override
public void onResponse(String response, int id) {
LogUtils.e(TAG, response);
}
});

保证手机和电脑在同一个局域网段中,运行app,发送网络请求,添加过滤tagOkHttp后可以在logat看到如下日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
System.err:     at com.colletion.android.MyApplication.initOkHttpClient(MyApplication.java:71)
04-19 22:10:54.258 17511-17568 E/OkHttp: ========request'log=======
04-19 22:10:54.258 17511-17568 E/OkHttp: method : GET
04-19 22:10:54.258 17511-17568 E/OkHttp: url : https://192.168.8.102:8443/
04-19 22:10:54.258 17511-17568 E/OkHttp: headers : Host: 192.168.8.102:8443
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.10.0
04-19 22:10:54.258 17511-17568 E/OkHttp: ========request'log=======end
04-19 22:10:54.405 17511-17568 E/OkHttp: ========response'log=======
04-19 22:10:54.405 17511-17568 E/OkHttp: url : https://192.168.8.102:8443/
04-19 22:10:54.405 17511-17568 E/OkHttp: code : 200
04-19 22:10:54.405 17511-17568 E/OkHttp: protocol : http/1.1
04-19 22:10:54.405 17511-17568 E/OkHttp: ========response'log=======end

上边的日志中有一个warn级别的信息System.err: at com.colletion.android.MyApplication.initOkHttpClient(MyApplication.java:71)开始自己没有注意到,后来通过断点发现这是因为在MyApplication中传入公钥证书的流之前调用了下边的这行代码将输入流close了,

1
FileUtil.safeCloseIs(severInputStream);

然后就有上边warn级别的错误日志。那么这个错误信息有什么影响呢。通过断点我们可以跟到com.zhy.http.okhttp.https.HttpsUtils这个类里边,下方的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//HttpsUtils类
public static SSLParams getSslSocketFactory(InputStream[] certificates, InputStream bksFile, String password)
{
SSLParams sslParams = new SSLParams();
try
{
TrustManager[] trustManagers = prepareTrustManager(certificates);
KeyManager[] keyManagers = prepareKeyManager(bksFile, password);
SSLContext sslContext = SSLContext.getInstance("TLS");
X509TrustManager trustManager = null;
if (trustManagers != null)
{
trustManager = new MyTrustManager(chooseTrustManager(trustManagers));
} else
{
trustManager = new UnSafeTrustManager();
}

因为我们调用HttpsUtils.getSslSocketFactory(new InputStream[]{severInputStream}, null, null)之前将本地的公钥证书输入流close了,所以prepareTrustManager(certificates)返回的trustManagers对象为null,所以接下来调用了trustManager = new UnSafeTrustManager();。 那么我们再来看UnSafeTrustManager,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static class UnSafeTrustManager implements X509TrustManager
{
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException
{
}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException
{
}

@Override
public X509Certificate[] getAcceptedIssuers()
{
return new java.security.cert.X509Certificate[]{};
}
}

可以看到checkServerTrusted()是一个空实现, 也就是不检查服务器是否可信,这样其实是不安全的,很容易出现https中间人攻击。 可以参考博客移动端 安卓apk安全测试 2017.8.14

所以要将MyApplication中的 FileUtil.safeCloseIs(severInputStream);这行代码注释掉。 然后运行程序就不会出现那个warn级别的错误提示了。

1
2
//                FileUtil.safeCloseIs(severInputStream);
// FileUtil.safeCloseIs(clientInputStream);

接下来我们采用fiddler进行抓包。会返回如下异常

1
java.io.IOException: unexpected end of stream on Connection{192.168.8.102:8443, proxy=HTTP@192.168.8.102:8884 hostAddress=/192.168.8.102:8884 cipherSuite=TLS_RSA_WITH_AES_128_CBC_SHA protocol=http/1.1}

在fiddler中我们可以看到如下错误,如图35-6

35-6-fiddler抓包1

上边异常和图中的错误信息表示:我们请求的服务器是192.168.8.102:8443, 但实际上返回给我们信息的是192.168.8.102:8884(fiddler代理),所以它就失败了。然后我们将fiddler关掉,同时将手机上的代理也取消掉,重新请求发网络请求,发现logcat打印如下日志,返回200,说明请求成功。

1
2
3
4
5
04-19 23:30:36.798 18130-20326 E/OkHttp: ========response'log=======
04-19 23:30:36.798 18130-20326 E/OkHttp: url : https://192.168.8.102:8443/
04-19 23:30:36.799 18130-20326 E/OkHttp: code : 200
04-19 23:30:36.799 18130-20326 E/OkHttp: protocol : http/1.1
04-19 23:30:36.799 18130-20326 E/OkHttp: ========response'log=======end

1.2.2 实现双向认证

上边单项认证是服务器对客户端的证书进行校验, 如果在客户端也会放一个jks的私钥证书,服务器端放一个cer的公钥证书,客户端会对服务器端的那个cer证书进行校验,这样两者互相都校验了的方式就称为双向认证。

和上边生成服务器证书的命令一样,在生成一对证书,分别为syc_client.kjssyc_client.cer

1
2
3
4
生成jks证书
keytool -genkey -alias syc_client -keyalg RSA -keystore syc_client.jks -validity 3600 -storepass 123456
根据jks证书生成(导出)cer证书
keytool -export -alias syc_client -file syc_client.cer -keystore syc_client.jks -storepass 123456

将cer证书放到/home/shaoyance/temp中, 然后修改服务端的配置,并重启服务。

1
2
3
4
5
6
<Connector SSLEnabled="true" acceptCount="100" clientAuth="true" truststoreFile="/home/shaoyance/temp/syc_client.cer"
disableUploadTimeout="true" enableLookups="true" keystoreFile="/home/shaoyance/temp/syc_server.jks" keystorePass="123456" maxSpareThreads="75"
maxThreads="200" minSpareThreads="5" port="8443"
protocol="org.apache.coyote.http11.Http11NioProtocol" scheme="https"
secure="true" sslProtocol="TLS"
/>

和配置单项认证不同的是这里将client的属性修改为了true,又增加了属性clientAuth="true" truststoreFile="/home/shaoyance/temp/syc_client.cer"

配置好了服务端接下来配置客户端,我将syc_client.kjs放到android项目assets目录中, 修改MyApplication代码如下,和单项认证对比,修改后的代码如下:

1
2
3
4
5
6
7
private static final String CLIENT_JKS ="syc_client.jks" ;
...
severInputStream = getApplicationContext().getAssets().open(SERVER_CER);
clientInputStream = getApplicationContext().getAssets().open(CLIENT_JKS);
...
//HttpsUtils.SSLParams sslParams = HttpsUtils.getSslSocketFactory(new InputStream[]{severInputStream}, null, null);
HttpsUtils.SSLParams sslParams = HttpsUtils.getSslSocketFactory(new InputStream[]{severInputStream}, clientInputStream, "123456");

上边代码中CLIENT_JKS指的就是我们刚才放在assets中的syc_client.kjs证书, HttpsUtils.SSLParams sslParams = HttpsUtils.getSslSocketFactory(new InputStream[]{severInputStream}, clientInputStream, "123456");第一个参数就是单项认证中存放到assets中的syc_server.cer服务器公钥证书,参数二表示的是syc_client.jks客户端私钥证书对应的流,参数三表示的是客户端私钥证书对应的密码。然后运行程序,发现会在logcat发现如下warn级别的日志

1
2
3
4
5
6
7
04-20 00:03:34.935 24868-24868 W/System.err: java.io.IOException: Wrong version of key store.
04-20 00:03:34.935 24868-24868 W/System.err: at com.android.org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.engineLoad(BcKeyStoreSpi.java:811)
04-20 00:03:34.935 24868-24868 W/System.err: at java.security.KeyStore.load(KeyStore.java:590)
04-20 00:03:34.935 24868-24868 W/System.err: at com.zhy.http.okhttp.https.HttpsUtils.prepareKeyManager(HttpsUtils.java:153)
04-20 00:03:34.935 24868-24868 W/System.err: at com.zhy.http.okhttp.https.HttpsUtils.getSslSocketFactory(HttpsUtils.java:41)
04-20 00:03:34.935 24868-24868 W/System.err: at com.colletion.android.MyApplication.initOkHttpClient(MyApplication.java:72)
04-20 00:03:34.935 24868-24868 W/System.err: at com.colletion.android.MyApplication.onCreate(MyApplication.java:48)

请求自然也是不成功的, 那么第72行代码到底发生什么错误呢? 经过断点调式发现在HttpsUtils的prepareKeyManager方法中抛了如下异常

1
Wrong version of key store.

这是因为java平台只识别jks的证书文件,而android平台只识别bks格式的证书文件。

证书的格式:
参考博客Android中SSL通信中使用的bks格式证书的生成中的评论。
实际上bks不是证书格式,很多人都误解了。bks是KeyStore文件的格式,而KeyStore是Java中的密钥仓库,里面不仅可以存放证书,还可以存放密钥。KeyStore可以有多种格式, 在Windows平台默认就是jks格式,而在Android上则默认是bks格式,这个bks是著名的Java加密库Bouncy Castle提供的。证书的编码格式只有两种PEM和DER格式,但是保存证书的文件格式可以有好多种,有什么cer、crt之类的

所以我们需要将syc_client.kjs转换成bks格式的证书。 打开链接下载转换工具,自己在电脑的/安装包/Linux/签名转换工具中也保存了一份。 解压后,进入解压目录,输入命令打开gui界面

1
java -jar portecle.jar

35-7-证书转换(转自鸿洋博客)

如果鸿洋的这种方式转换不成功可以参考jks转bks所遇到的那些坑

修改MyApplication代码如下

1
private static final String CLIENT_JKS ="syc_client.bks" ;

重新运行程序后,请求返回如下异常信息

1
java.net.SocketTimeoutException: SSL handshake timed out

自己排查了一下午也没有排查出来为什么, 然后参照博客Tomcat配置SSL双向认证重新将所有的证书重新配置了一遍,发现可以请求成功。为什会出现这种情况,以后有时间了再排查。

这篇博客主要讲了设置浏览器的双向认证,不过道理是一样的,浏览器和Android都属于客户端。新建一个目录来重新生成所有的证书。

生成服务器端证书

1
2
3
4
5
6
7
8
//生成服务端证书  192.168.8.102为 服务器的ip,此处也就是自己电脑在局域网段中的ip地址
keytool -genkeypair -v -alias server -keyalg RSA -validity 3650 -keystore ./server.keystore -storepass 111111 -keypass 111111 -dname "CN=192.168.8.102,OU=rm,O=rm,L=gz,ST=gd,C=cn"

//导出服务器端证书
keytool -exportcert -alias server -keystore ./server.keystore -file ./server.cer -storepass 111111

//将服务器端证书导入信任证书
keytool -importcert -alias serverca -keystore ./server_trust.keystore -file ./server.cer -storepass 111111

这时会在当前目录中生成三个文件server.keystore、server.cer、server_trust.keystore

生成客户端证书

1
2
3
4
5
6
//生成客户端证书
keytool -genkeypair -v -alias client -dname "CN=rorymo" -keyalg RSA -validity 3650 -keypass 111111 -keystore ./client.p12 -storepass 111111 -storetype PKCS12
//导出客户端证书
keytool -exportcert -alias client -file ./client.cer -keystore ./client.p12 -storepass 111111 -storetype PKCS12
//将客户端证书导入到服务器信任证书库
keytool -importcert -alias clientca -keystore ./server_trust.keystore -file ./client.cer -storepass 111111

这时会在当前目录中生成两个文件client.p12client.cer, 此时目录中公有五个证书,分别是server.keystore、server.cer、server_trust.keystore、client.p12、client.cer

然后配置tomcat应用, 我们还是将server.keystoreserver_trust.keystore放到/home/shaoyance/temp/中。 接着修改tomcat的server.xml文件,然后重启服务器。 代码如下

1
2
3
4
5
6
<Connector SSLEnabled="true" acceptCount="100" clientAuth="true" truststoreFile="/home/shaoyance/temp/server_trust.keystore"
disableUploadTimeout="true" enableLookups="true" keystoreFile="/home/shaoyance/temp/server.keystore" keystorePass="111111" maxSpareThreads="75"
maxThreads="200" minSpareThreads="5" port="8443"
protocol="org.apache.coyote.http11.Http11NioProtocol" scheme="https"
secure="true" sslProtocol="TLS"
/>

输入https://127.0.0.1:8443/,还是会报如下错误

35-4-1-tomcat-https 接受风险并继续

然后我点击高级—接受并继续, 此时并没有打开tomcat的首页, 会报如下35-8中的错误,这是因为我们采用了双向认证的缘故。

35-8-浏览器双向认证1

点击火狐浏览器的首选项—隐私与安全—查看证书—你的证书—导入,输入证书的口令,点击确定即可导入。

35-9-火狐浏览器导入证书

点击查看证书就可以查看证书的详细信息, 如图35-11.

35-11-火狐浏览器查看证书

这时我们点击重试,出现如下图所示弹框

35-10-浏览器双向认证2

点击确定后就可以打开tomcat的首页了,说明浏览器可以实现双向认证了,然后我们开始设置Android客户端。

我们仍然使用鸿洋博客中提供的转换工具将client.p12转换成bks格式的证书,自己将其命名为client.bks, 然后将server.cerclient.bks放到assets目录中。修改MyApplication的代码如下

1
2
3
private static final String SERVER_CER ="server.cer" ;
private static final String CLIENT_JKS ="client.bks" ;
HttpsUtils.SSLParams sslParams = HttpsUtils.getSslSocketFactory(new InputStream[]{severInputStream}, clientInputStream, "111111");

然后运行程序,发送网络请求会打印如下日志,说明网络请求成功了。

1
2
3
4
5
04-20 01:03:39.477 11787-11847 E/OkHttp: ========response'log=======
04-20 01:03:39.477 11787-11847 E/OkHttp: url : https://192.168.8.102:8443/
04-20 01:03:39.477 11787-11847 E/OkHttp: code : 200
04-20 01:03:39.477 11787-11847 E/OkHttp: protocol : http/1.1
04-20 01:03:39.477 11787-11847 E/OkHttp: ========response'log=======end

到这里双向认证的实践就算大工告成了。

参考博客:

写一篇最好懂的HTTPS讲解(郭霖)
Android Https相关完全解析 当OkHttp遇到Https(鸿洋)
Tomcat配置SSL双向认证(linux公社的文章)
jks转bks所遇到的那些坑