欢迎关注微信大众号:FSA全栈行动

一、简述

最近运用 OkHttp 拜访 https 恳求时,在个别 Android 设备上遇到了几个问题,搜罗网上材料,经过一番实践后,问题得到了处理,同时,我也同步晋级了我的 https 证书疏忽库 ANoSSL ,在此,对搜集到的材料和问题处理方案做个记载。

文章中的代码完成可到 GitHub 库房中自行获取:

二、协议

要想让 OkHttp 支撑 https 恳求,需求先对 https 证书协议以及 OkHttp 的支撑状况有个大概了解:

  1. 【服务端】https 证书是装备在服务端的,大体分为 SSLTLS 两种协议,TLS (Transport Layer Security) 是 SSL 的晋级版别,能够修正现有的 SSL 漏洞。
  2. 【客户端】OkHttp 支撑过的 https 证书协议有 SSLv3 (1996)、TLSv1 (1999)、TLSv1.1 (2006)、TLSv1.2 (2008) 和 TLSv1.3 (2018),但要留意,OkHttp 从 2014 年开端就抛弃对 SSLv3 支撑,2019 年(3.13.x)开端抛弃对 TLSv1TLSv1.1 的支撑,以 TLSv1.2 为最低支撑标准。

材料来历:

我找了几个网站,它们支撑的 https 证书协议支撑状况如下:

支撑协议 www.baidu.com www.fresco-cn.org api.github.com
TLS1.3 No No Yes
TLS1.2 Yes Yes Yes
TLS1.1 Yes Yes No
TLS1.0 Yes Yes No
SSL3.0 Yes No No
SSL2.0 No No No

数据来历:

能够看到,这几个网站都支撑 TLS1.2,而关于其他的 ssl 协议的支撑力度各不相同,现在来说,TLS1.2 才是干流,但有或许存在个别网站不支撑,所以,咱们在运用 OkHttp 发起 https 恳求之前,首先要搞清楚,便是服务端(接口)支撑的 ssl 协议有哪些。承认好服务端的 ssl 协议支撑状况后,就能够开端装备客户端的 OkHttp 了。

三、装备

这儿有个问题,是否只要发送 https 恳求,就一定需求给 OkHttp 装备 https 校验呢?答案对错必须的,正常状况下 OkHttp 会运用默许的系统装备,用于拜访一般的 https 恳求足以,但往往有一些特殊状况,就需求咱们在工程中进行单独装备并完成校验规矩,例如以下几种状况:

  1. 服务端运用了非 CA 认证的私有 https 证书
  2. 服务端运用了过期的 https 证书
  3. 客户端支撑某个 ssl 协议可是默许没有启用

好了,下面开端对 OkHttp 进行装备,大体分两步:

  1. 装备 SSLSocketFactory:用于指定支撑某种 ssl 协议的 SocketFactory
  2. 装备 HostnameVerifier:用于检查证书中的主机名与运用该证书的服务器的主机名是否一致
val sslSocketFactory = NoSSLSocketClient.getTLSSocketFactory()
val x509TrustManager = NoSSLSocketClient.getX509TrustManager()
val hostnameVerifier = NoSSLSocketClient.getHostnameVerifier()
val okHttpClient = OkHttpClient.Builder()
    .sslSocketFactory(
        sslSocketFactory,
        x509TrustManager // 必须指定该参数,否则 Android 10 及以上版别会闪退
    )
    .hostnameVerifier(hostnameVerifier)
    .build()

这儿主要看 sslSocketFactory 是怎样创立的,前面说过,https 证书大体分为 SSLTLS 两种协议,这儿的 SSLSocketFactory 也一样,以下是两种协议对应的创立方式,它们只是只是在获取 SSLContext 实例时传的参数不同罢了:

// SSL(不推荐)
public static SSLSocketFactory getSSLSocketFactory() {
    try {
        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, getTrustManager(), new SecureRandom());
        return sslContext.getSocketFactory();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
// TLS(推荐)
public static SSLSocketFactory getTLSSocketFactory() {
    try {
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, getTrustManager(), new SecureRandom());
        return sslContext.getSocketFactory();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

注:由于 TLSSSL 的晋级版别,且 SSLv3 现已弃用,TLSv1.2 是现在的干流,所以推荐运用 SSLContext.getInstance("TLS"),除非服务端证书只支撑 SSLv3,可是现在来说应该不太或许了。

四、问题

正确装备好 SSLSocketFactoryHostnameVerifier 之后,理论上就能够顺畅拜访 https 了,可是,在不同的安卓设备上,或许还是会出现拜访不通甚至溃散的状况。

1、Failure in SSL library, usually a protocol error

这个问题算是比较常见的,在搜索引擎里,随意一搜便是一堆,详细报错信息如下:

javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x4f859620: Failure in SSL library, usually a protocol error
error:1407742E:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert protocol version (external/openssl/ssl/s23_clnt.c:741 0x4c203d5c:0x00000000)
    at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:448)
    at okhttp3.internal.io.RealConnection.connectTls(RealConnection.java:239)
    ...
    Suppressed: javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x4f859620: Failure in SSL library, usually a protocol error

从反常信息中可知,SSL 握手中止,提示通常是协议问题,实际状况也根本如它所言,我遇到的有以下两种或许:

  1. 【客户端】设备上运用了署理,比如 Charles、Fiddler 这些抓包东西
  2. 【客户端】运用的证书协议与服务端支撑的不一致

第 1 种状况,能够先把署理去掉再排查,抓包东西不是本文的评论的内容,这儿就不展开了。
第 2 种状况,是给 OkHttp 装备的协议搞错了,或许是设备 “不支撑” 这个协议。

能够先承认 OkHttp 装备的 SSLSocketFactory 运用的协议是否为服务端支撑的协议,前面提到过怎样检查服务端支撑的协议,以及怎样给 OkHttp 装备对应协议的 SSLSocketFactory,相信这个很好排查承认。假如承认装备没有搞错,而且还是会报这个反常的话,就得考虑一下当前的客户端设备是否 “不支撑” 这个协议了?

留意:这儿的 “不支撑” 加了双引号,详细原因下面立刻解说。

下面是 Android 官方文档中,Socket 客户端证书支撑的状况表格,从表格中能够看到 TLSv1.1TLSv1.2 从 Android4.1(16) 开端就现已支撑了,从 Android 5.0(20) 开端默许启用,而 SSLv3 在 Android7.1(25) 之后就不再支撑:

Protocol Supported (API Levels) Enabled by default (API Levels)
SSLv3 1–25 1–22
TLSv1 1+ 1+
TLSv1.1 16+ 20+
TLSv1.2 16+ 20+
TLSv1.3 29+ 29+

表格来历:developer.android.com/reference/j…

这儿咱们只评论 TLS 的状况,现在 TLS1.2 是干流,一般咱们工程中给 OkHttp 装备支撑 TLSSSLSocketFactory,这是不会错的,可是现在遇到了这个过错,需求考虑一下咱们的 app 是否运行在了 Android4.x (或许一些魔改 ROM)的系统上,虽然从 Android4.1(16) 开端就现已支撑 TLSv1.2,可是默许没有启用,直到 Android 5.0(20) 才开端默许启用,咱们能够经过给 SSLSocketFactory 强制启用 TLSv1.2 来处理这个问题,这儿需求自定义一个 SSLSocketFactory

/**
 * 注:这儿重载了 n 个 createSocket(...) 方法,由于篇幅问题省略掉了
 * 详见 https://github.com/GitLqr/ANoSSL/blob/main/anossl/src/main/java/com/gitlqr/anossl/TLSSocketFactory.java
 */
public class TLSSocketFactory extends SSLSocketFactory {
    ...
    @Override
    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
        return enableTLSOnSocket(delegate.createSocket(address, port, localAddress, localPort));
    }
    private Socket enableTLSOnSocket(Socket socket) {
        if ((socket instanceof SSLSocket)) {
            ((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.2"});
        }
        return socket;
    }
}

至此,这个问题在咱们项目里就不再出现了。另外,OkHttp 从 2014 年开端就抛弃对 SSLv3 支撑,2019 年(3.13.x)开端抛弃对 TLSv1TLSv1.1 的支撑,以 TLSv1.2 为最低支撑标准,这儿也是一个坑点,假如服务端不支撑 TLSv1.2,只支撑 SSLv3TLSv1.1 这些旧协议的话,那么你能够经过下降 OkHttp 版别(比如:3.12.x)来予以支撑。

注:通常状况下,咱们会以服务器装备的 https 证书为准,所以都是优先从客户端下手处理,真实没办法的话,再考虑让服务端合作调整,主张运用 TLSv1.2

2、getEnabledProtocols() 回来值类型转化反常

这是一个适当奇葩的问题,详细报错信息如下:

java.lang.ClassCastException: int[] cannot be cast to java.lang.String[]
    at com.android.org.conscrypt.OpenSSLSocketImpl.getEnabledProtocols(OpenSSLSocketImpl.java:802)
    at okhttp3.ConnectionSpec.isCompatible(ConnectionSpec.java:207)
    at okhttp3.internal.connection.ConnectionSpecSelector.configureSecureSocket(ConnectionSpecSelector.java:60)
    at okhttp3.internal.connection.RealConnection.connectTls(RealConnection.java:313)
    at okhttp3.internal.connection.RealConnection.establishProtocol(RealConnection.java:284)
    at okhttp3.internal.connection.RealConnection.connect(RealConnection.java:169)
    ...

定位到 OkHttp 源码 ConnectionSpec.java:207 处,调用了 socket.getEnabledProtocols() 这句代码,检查该接口方法声明如下:

// android.jar javax.net.ssl.SSLSocket
public abstract class SSLSocket extends Socket {
    /**
     * Returns the names of the protocol versions which are currently
     * enabled for use on this connection.
     * @see #setEnabledProtocols(String [])
     * @return an array of protocols
     */
    public abstract String [] getEnabledProtocols();
    ...
}

这儿分明回来的便是 String[],不是 int[],可是为啥还给我报类型转化反常过错呢?莫非是一些接口在魔改 ROM 里被修正了吗?网上找不到与之相关的问题,百思不得其解,最终没办法,只能另辟蹊径了,前面自定义 SSLSocketFactory 的时分,咱们重载了各个 Socket createSocket() 方法来强制启用 TLSv1.2,这个回来的 Socket 正好便是 SSLSocket,于是抱着试一试的心态,自定义 SSLSocket 并重写 setEnabledProtocols() 方法得以处理:

/**
 * 注:DelegateSSLSocket 只是一个包装类罢了
 * 详见:https://github.com/GitLqr/ANoSSL/blob/main/anossl/src/main/java/com/gitlqr/anossl/DelegateSSLSocket.java
 */
public class TLSSocketFactory extends SSLSocketFactory {
    private final String[] enabledProtocols = {"TLSv1.2"};
    ...
    @Override
    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
        return enableTLSOnSocket(delegate.createSocket(address, port, localAddress, localPort));
    }
    private Socket enableTLSOnSocket(Socket socket) {
        if ((socket instanceof SSLSocket)) {
            // 20240405:
            // Android 4.4 及以下版别或许存在一些奇葩问题,需求自己完成了一个 DelegateSSLSocket 来处理,
            // 可是 Android 5.0 及以上不要运用,OkHttp 在高版别中会调用一些 DelegateSSLSocket 没有复写的方法,导致 app 溃散。
            if (isLtAndroid5()) {
                socket = new DelegateSSLSocket((SSLSocket) socket) {
                    @Override
                    public void setEnabledProtocols(String[] protocols) {
                        // super.setEnabledProtocols(protocols);
                        super.setEnabledProtocols(enabledProtocols);
                    }
                };
            } else {
                ((SSLSocket) socket).setEnabledProtocols(enabledProtocols);
            }
        }
        return socket;
    }
}

现在发现该问题只出现在 极个别 的 Android 4.x 设备上,在高版别 Android 系统上并未发现,所以,为了下降危险,将上述代码做了系统版别控制,运行状况是否稳定还在观察中。

好了,以上便是本篇文章的全部内容了,假如对你有协助的话,请不惜点个赞,也能够关注我,不守时发布实践心得。

假如文章对您有所协助, 请不惜点击关注一下我的微信大众号:FSA全栈行动, 这将是对我最大的鼓励. 大众号不仅有Android技术, 还有iOS, Python等文章, 或许有你想要了解的技术知识点哦~