Android - OkHttp 访问 https 的怪问题

欢迎关注微信公众号: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:用于检查证书中的主机名与使用该证书的服务器的主机名是否一致
kotlin 复制代码
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 实例时传的参数不同而已:

java 复制代码
// 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

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

java 复制代码
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

java 复制代码
/**
 * 注:这里重载了 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 复制代码
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() 这句代码,查看该接口方法声明如下:

java 复制代码
// 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() 方法得以解决:

java 复制代码
/**
 * 注: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等文章, 可能有你想要了解的技能知识点哦~

相关推荐
七月.末1 小时前
安卓aab包的安装教程,附带adb环境的配置
android·adb
SRC_BLUE_176 小时前
SQLI LABS | Less-39 GET-Stacked Query Injection-Intiger Based
android·网络安全·adb·less
无尽的大道9 小时前
Android打包流程图
android
镭封11 小时前
android studio 配置过程
android·ide·android studio
夜雨星辰48711 小时前
Android Studio 学习——整体框架和概念
android·学习·android studio
邹阿涛涛涛涛涛涛11 小时前
月之暗面招 Android 开发,大家快来投简历呀
android·人工智能·aigc
IAM四十二11 小时前
Jetpack Compose State 你用对了吗?
android·android jetpack·composer
奶茶喵喵叫12 小时前
Android开发中的隐藏控件技巧
android
wdxylb12 小时前
将C++搭建的简单HTTP服务器升级为 HTTPS 服务器
运维·服务器·https
Winston Wood13 小时前
Android中Activity启动的模式
android