Android 与服务端使用 Https 加密通信

Https 证书

现在网络安全越来越受重视,通用做法是采用 https 加密通信,使用 https 需要数字证书,只有合法的证书才能被浏览器、操作系统默认支持。

所谓的合法证书其实是在 CA 公司那购买的(原来我们的合法性是花钱从别人那买来的,不得不吐槽这种互联网安全设计真是坑爹),虽然现在也有一些免费 CA 证书,但申请还是挺麻烦,这里我们使用自签名的 https 证书,基于这种没有经过 CA 机构签名的证书,也可以实现 Android 与服务端之间的加密通信。

服务端使用 https

生成 https 证书

JDK 自带的 keytool 工具可以很方便生成 https 证书,可以查看它的使用方法:

比如这条命令就可以生成一个有效期 10 年的证书:

1
keytool -genkey -alias spring -validity 3560 -keystore spring.keystore

服务端配置 https 证书

服务端一般使用 Tomcat、Jetty、Undertow 等作为 Servlet 容器,我们将上面生成的 keystore 证书放在项目中,然后在配置文件中引入证书即可:

1
2
3
4
server.ssl.key-store=spring.keystore
server.ssl.key-alias=spring
server.ssl.key-password=password
server.ssl.key-store-type=JKS

这样服务端就支持 https 了,启动项目访问服务就要加 https 前缀,如 https://localhost:8080/user

Android 自定义 https 校验

如果是花钱买的 CA 证书是不需要额外配置的,Android 系统内部有信任列表,会自行校验通过,这里讲配置自定义 https 校验。

Android 端通常使用 Retrofit 做网络请求,Retrofit 底层就是 OKhttp,OKhttp 实现自定义 https 校验并不难,主要分三步。

把证书公钥预埋在 APP 中

这条命令可以导出证书公钥字符串:

1
keytool -list -rfc -keystore spring.keystore

把这个公钥作为一个字符串常量放在项目中供后面校验使用。

自定义证书校验逻辑

使用上面的公钥字符串构建 X509TrustManager 对象,在 checkServerTrusted 方法中校验服务端证书:

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
X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
//校验客户端证书
}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
//校验服务端证书
X509Certificate ca = (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(PUB_KEY.getBytes()));

for (X509Certificate cert : chain) {
// 检查服务端证书是否过期
cert.checkValidity();
//和APP预埋证书对比
cert.verify(ca.getPublicKey());
//证书校验异常时抛出一个CertificateException
}
}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};

设置 OkHttpClient 校验证书

然后使用上面的 trustManager 构建 OkHttpClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SSLSocketFactory sslSocketFactory = null;
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{trustManager}, new SecureRandom());
sslSocketFactory = sslContext.getSocketFactory();

} catch (NoSuchAlgorithmException | KeyManagementException e) {
e.printStackTrace();
}
final HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(final String hostname,
final SSLSession session) {
//服务端主机域名地址校验
return true;
}
};
OkHttpClient client = new OkHttpClient.Builder()
.hostnameVerifier(hostnameVerifier)
.sslSocketFactory(sslSocketFactory, trustManager)
.build();

使用这个配置好的 OkHttpClient 与服务端交互,就可以支持自定义 https 证书加密通信了,如果服务端证书不符,请求会自动断开。