Android - OkHttp 访问 https 的怪问题

发布于:2024-04-29 ⋅ 阅读:(26) ⋅ 点赞:(0)

一、简述

最近使用 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(