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等文章, 可能有你想要了解的技能知识点哦~

相关推荐
GEEKVIP2 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
model20054 小时前
android + tflite 分类APP开发-2
android·分类·tflite
彭于晏6894 小时前
Android广播
android·java·开发语言
与衫5 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
叶北辰CHINA6 小时前
nginx反向代理,负载均衡,HTTP配置简述(说人话)
linux·运维·nginx·http·云原生·https·负载均衡
前端李易安8 小时前
ajax的原理,使用场景以及如何实现
前端·ajax·okhttp
GodK77711 小时前
HTTPS 的加密流程
网络协议·http·https
500了11 小时前
Kotlin基本知识
android·开发语言·kotlin
人工智能的苟富贵12 小时前
Android Debug Bridge(ADB)完全指南
android·adb
小雨cc5566ru17 小时前
uniapp+Android面向网络学习的时间管理工具软件 微信小程序
android·微信小程序·uni-app