Android - OkHttp 访问 https 的怪问题
# 一、简述
最近使用 OkHttp 访问 https 请求时,在个别 Android 设备上遇到了几个问题,搜罗网上资料,经过一番实践后,问题得到了解决,同时,我也同步升级了我的 https 证书忽略库 ANoSSL (opens new window) ,在此,对搜集到的资料和问题解决方案做个记录。
文章中的代码实现可到 GitHub 仓库中自行获取:
# 二、协议
要想让 OkHttp 支持 https 请求,需要先对 https 证书协议以及 OkHttp 的支持情况有个大概了解:
- 【服务端】https 证书是配置在服务端的,大体分为
SSL
和TLS
两种协议,TLS (Transport Layer Security) 是 SSL 的升级版本,可以修复现有的 SSL 漏洞。 - 【客户端】OkHttp 支持过的 https 证书协议有 SSLv3 (1996)、TLSv1 (1999)、TLSv1.1 (2006)、TLSv1.2 (2008) 和 TLSv1.3 (2018),但要注意,OkHttp 从 2014 年开始就放弃对
SSLv3
支持,2019 年(3.13.x)开始放弃对TLSv1
和TLSv1.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 请求足以,但往往有一些特殊情况,就需要我们在工程中进行单独配置并实现校验规则,例如以下几种情况:
- 服务端使用了非 CA 认证的私有 https 证书
- 服务端使用了过期的 https 证书
- 客户端支持某个 ssl 协议但是默认没有启用
好了,下面开始对 OkHttp 进行配置,大体分两步:
- 配置
SSLSocketFactory
:用于指定支持某种 ssl 协议的 SocketFactory - 配置
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 证书大体分为 SSL
和 TLS
两种协议,这里的 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);
}
}
注:因为
TLS
是SSL
的升级版本,且SSLv3
已经弃用,TLSv1.2
是现在的主流,所以推荐使用SSLContext.getInstance("TLS")
,除非服务端证书只支持SSLv3
,但是目前来说应该不太可能了。
# 四、问题
正确配置好 SSLSocketFactory
和 HostnameVerifier
之后,理论上就可以顺利访问 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 握手中断,提示通常是协议问题,实际情况也基本如它所言,我遇到的有以下两种可能:
- 【客户端】设备上使用了代理,比如 Charles、Fiddler 这些抓包工具
- 【客户端】使用的证书协议与服务端支持的不一致
第 1 种情况,可以先把代理去掉再排查,抓包工具不是本文的讨论的内容,这里就不展开了。 第 2 种情况,是给 OkHttp 配置的协议搞错了,或者是设备 "不支持" 这个协议。
可以先确认 OkHttp 配置的 SSLSocketFactory
使用的协议是否为服务端支持的协议,前面提到过怎么查看服务端支持的协议,以及怎么给 OkHttp 配置对应协议的 SSLSocketFactory
,相信这个很好排查确认。如果确认配置没有搞错,并且还是会报这个异常的话,就得考虑一下当前的客户端设备是否 "不支持" 这个协议了?
注意:这里的 "不支持" 加了双引号,具体原因下面马上解释。
下面是 Android 官方文档中,Socket 客户端证书支持的情况表格,从表格中可以看到 TLSv1.1
和 TLSv1.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+ |
表格来源:https://developer.android.com/reference/javax/net/ssl/SSLSocket (opens new window)
这里我们只讨论 TLS
的情况,目前 TLS1.2
是主流,一般我们工程中给 OkHttp 配置支持 TLS
的 SSLSocketFactory
,这是不会错的,但是现在遇到了这个错误,需要考虑一下我们的 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)开始放弃对 TLSv1
和 TLSv1.1
的支持,以 TLSv1.2
为最低支持标准,这里也是一个坑点,如果服务端不支持 TLSv1.2
,只支持 SSLv3
或 TLSv1.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 系统上并未发现,所以,为了降低风险,将上述代码做了系统版本控制,运行情况是否稳定还在观察中。
好了,以上便是本篇文章的全部内容了,如果对你有帮助的话,请不吝点个赞,也可以关注我,不定时发布实践心得。
- 01
- Flutter - 危!3.24版本苹果审核被拒!11-13
- 02
- Flutter - 轻松搞定炫酷视差(Parallax)效果09-21
- 03
- Flutter - 轻松实现PageView卡片偏移效果09-08