一、背景
項目中,客戶端與服務端之間普遍使用Https協議通信,突然接到測試同事反饋Android5.0以下手機上,App測試服使用出現問題,出現SSL handshake aborted
錯誤信息,但正式服正常。經查,普遍錯誤信息詳情如下:
SSL handshake aborted: ssl=0x78f08cd0: I/O error during system call, Connection reset by peer .... 復制代碼
從錯誤信息上粗略看上去,SSL握手階段出現問題,連接終止。
二、分析與處理
2.1 問題分析
從總體信息上看,顯然測試服與正式服環境有所不同,導致在Android 5.0以下機型上SSL握手階段失敗。很有可能是測試服改變了相關配置。網上查了一圈,很快,Android官網文檔上找到了對應指引。
developer.android.com/reference/j…
文檔中的圖示給出了Android版本與SSL/TLS版本之間的對應關系。

SSLSocket
來源於javax.net.ssl
,實際上是java的擴展庫,Android不同系統版本中引入的是不同的Open JDK版本。因此,此處的版本對應關系的背后,實際上是因為java的不同版本中,對於SSLSocket
中對SSL/TLS版本的默認支持發生了變化。
同時,對於更底層的,如SSLSocketFactoryImpl、SSLSocketImpl等實現類,Oracle JD是在sun.security.ssl
包中,Open JDK對其進行了自己的實現,並放在了com.android.org.conscrypt
包中。並在類名前前統一加上了Open
加以區分,如OpenSSLSocketFactoryImpl
、OpenSSLSocketImpl
等。
對於conscrypt的介紹,可以參考文檔:
source.android.google.cn/devices/arc…
Android 5.0 API級別是21,5.0以下常用的機型是4.4.x/4.2.x等。API Level 16對應的是Android 4.1。因此,問題基本上可以定位在服務端對TLS版本做了升級。
通過Https通信,客戶端與服務端在SSL/TLS層建立安全連接前,涉及到版本協商過程。SSL/TLS在客戶端和服務端分別具有對應的版本,握手階段客戶端與服務端的SSL/TLS版本,會取用兩者同時支持的最高版本。如在Android 4.4手機上,默認支持SSLv3,TLSv1,如果服務端配置支持的協議版本是TLSv1,TLSv1.1,TLSv1.2,則會取用TLSv1作為協商后的版本。當然,無論是客戶端還是服務端,對於SSL/TLS版本,在對應系統版本所能支持的協議版本范圍內,是可以人為去修改的。如4.4系統手機上,可以將客戶端在請求時的支持版本改成SSLv3,TLSv1,TLSv1.1,TLSv1.2。如果此時服務端支持的協議版本是TLSv1,TLSv1.1,TLSv1.2,協商后的版本將是TLSv1.2。
對於給定的Https的服務端網址,可以檢測其當前所支持的SSL/TLS版本。
推薦一個非常實用的檢測網站,不僅列出了服務端當前支持的版本,還列出了具體的加密套件等有用信息。
對項目中的正式服和測試服實測,結果如下:
正式服:

測試服:

顯然,服務端在測試服中,將TLS1.0的版本支持給直接去掉了。這也正好與測試結果及從Android官方文檔中分析結果是一致的。
經與服務端/運維等同事確認,測試服TLS版本協議確實做了修改,處於安全及升級等方面考慮,測試服運行一段時間后,后續也會同樣部署到正式服中。
這也就意味着,客戶端是需要適配的。
2.2 問題處理
當前項目最低支持版本是4.4,從Android官方文檔中可以看出,Android 4.4默認支持的SSL/TLS版本是SSLv3,TLSv1。但TLSv1.1,TLSv1.2實際上也是在其支持范圍內的,需要人為去配置。
我們的最終目標是改變改變SSLSocket
實例中的enabledProtocols
,具體可以通過調用其方法setEnabledProtocols(String protocols[])
。SSLSocket
,對外,是通過SSLSocketFactory
接口的方式與外部交互,其創建的調用方式,具體是通過createSocket()
方法進行。
因此,我們可以通過代理模式,設置兼容的SSLSocketFactory
,並重寫其對應的createSocket()
方法,同時,將其設置給OkHttpClient
的sslSocketFactory
。
首先實現代理類:
public class TlsCompatSocketFactory extends SSLSocketFactory {
private static final String[] TLS_VERSION_LIST = {"TLSv1", "TLSv1.1", "TLSv1.2"}; final SSLSocketFactory target; public TlsCompatSocketFactory(SSLSocketFactory target) { this.target = target; } @Override public String[] getDefaultCipherSuites() { return target.getDefaultCipherSuites(); } @Override public String[] getSupportedCipherSuites() { return target.getSupportedCipherSuites(); } @Override public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { return supportTLS(target.createSocket(s, host, port, autoClose)); } @Override public Socket createSocket(String host, int port) throws IOException, UnknownHostException { return supportTLS(target.createSocket(host, port)); } @Override public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { return supportTLS(target.createSocket(host, port, localHost, localPort)); } @Override public Socket createSocket(InetAddress host, int port) throws IOException { return supportTLS(target.createSocket(host, port)); } @Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { return supportTLS(target.createSocket(address, port, localAddress, localPort)); } private Socket supportTLS(Socket s) { if (s instanceof SSLSocket) { ((SSLSocket) s).setEnabledProtocols(TLS_VERSION_LIST); } return s; } } 復制代碼
然后在OkHttpClient的封裝類中,將其設置給OkHttpClient的builder:
....
@JvmStatic
// 設置5.0以下機型可以支持TLS 1.1/1.2版本
val sc = SSLContext.getInstance("TLS") sc.init(null, null, null) clientBuilder.sslSocketFactory(TlsCompatSocketFactory(sc.socketFactory), object : X509TrustManager { @Throws(CertificateException::class) override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) { } @Throws(CertificateException::class) override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) { } override fun getAcceptedIssuers(): Array<X509Certificate> { return arrayOf() } }) .... 復制代碼
設置完成后,如下圖所示,在進行Https請求時,sslSocketFactory
已經被替換成了我們自己定義的代理類TlsCompatSocketFactory
。其內部的target
對象中的sslParameters
的enabledProtocols
為TLSv1和SSlv3,此參數為創建SSLSocket
對象時默認的SSL/TLS協議版本。現在通過代理后,SSLSocket
對象中的enabledProtocols
已經變更成我們自定義的TLS_VERSION_LIST
,即同時包含了TLSv1、TLSv1.1、TLSv1.2協議版本。

另外,SSL/TLS協議版本中,還有一點需要注意的是,除了SSLSocket
對象支持的協議版本外,OkHttp還通過connectionSpecs
指定了一個連接規格
,連接規格
中,包含有tlsVersions
,此參數與SSLSocket
中的enabledProtocols
一起,用來控制實際連接建立時的終端SSL/TLS協議版本。
默認的取值邏輯如下:
static final List<ConnectionSpec> DEFAULT_CONNECTION_SPECS = Util.immutableList(
ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);
復制代碼
其中,MODERN_TLS
對應的實現如下:
public static final ConnectionSpec MODERN_TLS = new Builder(true) .cipherSuites(APPROVED_CIPHER_SUITES) .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0) .supportsTlsExtensions(true) .build(); 復制代碼
也就是說,默認情況下,ConnectionSpec
中的tlsVersions
對當前主流的SSL/TLS協議版本都是支持的。當然,特殊情況下,我們也可以人為去設置ConnectionSpec
並指定其內部的tlsVersions
。
下面我們看下tlsVersions
與enabledProtocols
取交集的具體邏輯。
private fun supportedSpec(sslSocket: SSLSocket, isFallback: Boolean): ConnectionSpec {
....
val tlsVersionsIntersection = if (tlsVersionsAsString != null) { sslSocket.enabledProtocols.intersect(tlsVersionsAsString, naturalOrder()) } else { sslSocket.enabledProtocols } .... return Builder(this) .cipherSuites(*cipherSuitesIntersection) .tlsVersions(*tlsVersionsIntersection) .build() } /** Applies this spec to {@code sslSocket}. */ void apply(SSLSocket sslSocket, boolean isFallback) { ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback); if (specToApply.tlsVersions != null) { sslSocket.setEnabledProtocols(specToApply.tlsVersions); } if (specToApply.cipherSuites != null) { sslSocket.setEnabledCipherSuites(specToApply.cipherSuites); } } 復制代碼
也就是說,最終sslSocket
實例中的enabledProtocols
,除了基於TlsCompatSocketFactory
中對sslSocket
設置的enabledProtocols
外,最終還會和ConnectionSpec
內部的tlsVersions
取交集后,再次賦值給sslSocket
實例中的enabledProtocols
。
三、結語
總體上來說,Https通信時,SSL/TLS的協議版本,在客戶端,首先取決於Android系統默認支持下的協議版本,並與ConnectionSpec
內部的tlsVersions
取交集,在服務端則依賴於服務端的配置。在握手階段,客戶端會和服務端協商最終的協議版本,取用兩者同時支持的最高版本。
一般情況下,終端可以盡量放寬協議版本,這樣當服務端更改協議版本,甚至只支持某一個協議版本(如TLSv1.1)時,在協議版本協商階段,都是可以有盡量匹配的版本,從而對Https通信不造成影響。當然,Android 5.0以上的系統中,默認情況下,客戶端對主流的協議版本都是支持的,一般不用做特殊處理。
end~
作者:HappyCorn
鏈接:https://juejin.im/post/5df8c7006fb9a01606716ba9
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。