作为前端开发,应该或多或少的都会熟悉HTTPS,特别是邻居家IOS,早就必须使用HTTPS了,Android也在9.0的时候增加了这一要求,当我们的targetSdkVersion指到9.0以上时,必须使用HTTPS。
HTTPS
简单来讲,HTTPS可以理解为 HTTP + SSL ,至于具体的HTTPS机制,涉及到一系列的加密,证书。。。。这边就不多做说明了,这里推荐郭神的一篇文章《写一篇最好懂的HTTPS讲解》,写的特别通俗易懂。
跳过HTTPS
这里简单说一句,Android 9.0 之后谷歌要求默认使用加密连接,如果我们targetSdkVersion指到9.0以上,我们所有的http请求,包括webview中加载的http链接,都将限制访问,但是并没有强制要求,所以还是有办法.
- targetSdkVersion 降到9.0以下。
- 在AndroidManifest.xml中application标签下,增加android:usesCleartextTraffic=“true”
- 在AndroidManifest.xml中application标签下,配置networkSecurityConfig,在对应的xml中可以配置所有地址或单独某个地址的请求是否使用加密传输。
如果只能使用HTTP,这里推荐第三种,比较灵活。
威胁描述
使用HTTPS协议进行通信时,客户端需要对服务器身份进行完整性校验,以确认服务器是真实合法的目标服务器。如果没有校验,客户端可能与仿冒的服务器建立通信链接,即“中间人攻击”。Android允许开发者重定义证书验证方法,使用HostnameVerifier类检查证书中的主机名与使用该证书的服务器的主机名是否一致。如果重写的HostnameVerifier不对服务器的主机名进行验证,即验证失败时也继续与服务器建立通信链接,存在发生“中间人攻击”的风险。发生“中间人攻击”时,仿冒的中间人可以冒充服务器和客户端通信,也可以冒充客户端与服务器通信,从而截获通信内容,获取用户隐私信息。
域名验证
setHostnameVerifier
OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)//设置连接超时
.readTimeout(30, TimeUnit.SECONDS)//读取超时
.writeTimeout(30, TimeUnit.SECONDS)//写入超时
.addInterceptor(interceptor)//添加日志拦截器
.sslSocketFactory(createSSLSocketFactory(), new MyTrustManager())
.hostnameVerifier(hnv)
.build();
这里关注 hostnameVerifier
方法,看下注释。
我们实现我们自己的HostnameVerifier,这里可以把我们服务端的域名存在本地,和请求中的域名比较是否一致。
HostnameVerifier hnv = (hostname, session) -> {
if ((Config.DOMAIN_NAME).equals(hostname)) {
return true;
} else {
HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
return hv.verify(Config.DOMAIN_NAME, session);
}
};
证书验证
这里关注我们上面建造OkHttpClient时,设置的sslSocketFactory(createSSLSocketFactory(), new MyTrustManager())
方法
,其中MyTrustManager
实现了 X509TrustManager
,在checkServerTrusted(X509Certificate[] chain, String authType)
中验证证书.代码如下:
public class MyTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
//检查所有证书
try {
TrustManagerFactory factory = TrustManagerFactory.getInstance("X509");
factory.init((KeyStore) null);
for (TrustManager trustManager : factory.getTrustManagers()) {
((X509TrustManager) trustManager).checkServerTrusted(chain, authType);
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
}
//获取网络中的证书信息
X509Certificate certificate = chain[0];
// 证书拥有者
String subject = certificate.getSubjectDN().getName();
// 证书颁发者
String issuer = certificate.getIssuerDN().getName();
if (!Config.subject.equals(subject) || !Config.issuer.equals(issuer)){
throw new CertificateException();
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
这里我把证书中的拥有者和颁发者信息存在本地,在校验证书时,只校验了这两条。是因为我们的证书中存在很多信息,但是有的信息可能会改变,例如过期时间,如果我们完全都校验,可能会存在一些问题。正好说到这,推荐大家增加证书管理,这样才能避免其他的问题。