作为前端开发,应该或多或少的都会熟悉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链接,都将限制访问,但是并没有强制要求,所以还是有办法.

  1. targetSdkVersion 降到9.0以下。
  2. 在AndroidManifest.xml中application标签下,增加android:usesCleartextTraffic=“true”
  3. 在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 方法,看下注释。

Android app证书检查 安卓https证书_HTTPS


我们实现我们自己的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];
    }
}

这里我把证书中的拥有者和颁发者信息存在本地,在校验证书时,只校验了这两条。是因为我们的证书中存在很多信息,但是有的信息可能会改变,例如过期时间,如果我们完全都校验,可能会存在一些问题。正好说到这,推荐大家增加证书管理,这样才能避免其他的问题。