欢迎关注微信大众号:FSA全栈行动
一、简述
最近运用 OkHttp 拜访 https 恳求时,在个别 Android 设备上遇到了几个问题,搜罗网上材料,经过一番实践后,问题得到了处理,同时,我也同步晋级了我的 https 证书疏忽库 ANoSSL ,在此,对搜集到的材料和问题处理方案做个记载。
文章中的代码完成可到 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+ |
这儿咱们只评论 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 系统上并未发现,所以,为了下降危险,将上述代码做了系统版别控制,运行状况是否稳定还在观察中。
好了,以上便是本篇文章的全部内容了,假如对你有协助的话,请不惜点个赞,也能够关注我,不守时发布实践心得。
假如文章对您有所协助, 请不惜点击关注一下我的微信大众号:FSA全栈行动, 这将是对我最大的鼓励. 大众号不仅有Android技术, 还有iOS, Python等文章, 或许有你想要了解的技术知识点哦~