在Android中使用HTTPS的常见问题
发布日期:2018-05-04(1) 服务器证书验证错误
这是最常见的一种问题,通常会抛出如下类型的异常:
出现此类错误通常可能由以下的三种原因导致:
- 颁发服务器证书的CA未知;
- 服务器证书不是由CA签署的,而是自签署(比较常见);
- 服务器配置缺少中间 CA;
当服务器的CA不被系统信任时,就会发生 SSLHandshakeException。可能是购买的CA证书比较新,Android系统还未信任,也可能是服务器使用的是自签名证书(这个在测试阶段经常遇到)。
解决此类问题常见的做法是:指定HttpsURLConnection信任特定的CA集合。在本文的第5部分代码实现模块,会详细的讲解如何让Android应用信任自签名证书集合或者跳过证书校验的环节。
(2) 域名验证失败
SSL连接有两个关键环节。首先是验证证书是否来自值得信任的来源,其次确保正在通信的服务器提供正确的证书。如果没有提供,通常会看到类似于下面的错误:
出现此类问题的原因通常是由于服务器证书中配置的域名和客户端请求的域名不一致所导致的。
有两种解决方案:
- 重新生成服务器的证书,用真实的域名信息;
- 自定义HostnameVerifier,在握手期间,如果URL的主机名和服务器的标识主机名不匹配,则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。可以通过自定义HostnameVerifier实现一个白名单的功能。
代码如下:
HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { // 设置接受的域名集合 if (hostname.equals(...)) { return true; } } }; HttpsURLConnection.setDefaultHostnameVerifier(DO_NOT_VERIFY);
(3) 客户端证书验证
SSL支持服务端通过验证客户端的证书来确认客户端的身份。这种技术与TrustManager的特性相似。本文将在第5部分代码实现模块,讲解如何让Android应用支持客户端证书验证的方式。
(4) Android上TLS版本兼容问题
之前在接口联调的过程中,测试那边反馈过一个问题是在Android 4.4以下的系统出现HTTPS请求不成功而在4.4以上的系统上却正常的问题。相应的错误如下:
03-09 09:21:38.427: W/System.err(2496): javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0xb7fa0620: Failure in SSL library, usually a protocol error 03-09 09:21:38.427: W/System.err(2496): error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:741 0xa90e6990:0x00000000)
按照官方文档的描述,Android系统对SSL协议的版本支持如下:
Name Supported (API Levels) Default 10+ SSL 10–TBD SSLv3 10–TBD TLS 1+ TLSv1 10+ TLSv1.1 16+ TLSv1.2 16+
也就是说,按官方的文档显示,在API 16+以上,TLS1.1和TLS1.2是默认开启的。但是实际上在API 20+以上才默认开启,4.4以下的版本是无法使用TLS1.1和TLS 1.2的,这也是Android系统的一个bug。
参照stackoverflow上的一些方式,比较好的一种解决方案如下:
SSLSocketFactory noSSLv3Factory; if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { noSSLv3Factory = new TLSSocketFactory(mSSLContext.getSSLSocket().getSocketFactory()); } else { noSSLv3Factory = mSSLContext.getSSLSocket().getSocketFactory(); }
对于4.4以下的系统,使用自定义的TLSSocketFactory,开启对TLS1.1和TLS1.2的支持,核心代码:
public class TLSSocketFactory extends SSLSocketFactory { private SSLSocketFactory internalSSLSocketFactory; public TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException { SSLContext context = SSLContext.getInstance("TLS"); context.init(null, null, null); internalSSLSocketFactory = context.getSocketFactory(); } public TLSSocketFactory(SSLSocketFactory delegate) throws KeyManagementException, NoSuchAlgorithmException { internalSSLSocketFactory = delegate; } ...... @Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); } // 开启对TLS1.1和TLS1.2的支持 private Socket enableTLSOnSocket(Socket socket) { if(socket != null && (socket instanceof SSLSocket)) { ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.1", "TLSv1.2"}); } return socket; } }
代码实现
本部分主要基于第四部分提出的Android应用中使用HTTPS遇到的一些常见的问题,给出一个比较系统的解决方案。
(1) 整体结构
不管是使用自签名证书,还是采取客户端身份验证,核心都是创建一个自己的KeyStore,然后使用这个KeyStore创建一个自定义的SSLContext。整体类图如下:
类图中的MySSLContext可以应用在HTTPUrlConnection的方式与服务端连接的过程中:
if (JarConfig.__self_signed_https) { SSLContextByTrustAll mSSLContextByTrustAll = new SSLContextByTrustAll(); MySSLContext mSSLContext = new MySSLContext(mSSLContextByTrustAll); SSLSocketFactory noSSLv3Factory; if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { noSSLv3Factory = new TLSSocketFactory(mSSLContext.getSSLSocket().getSocketFactory()); } else { noSSLv3Factory = mSSLContext.getSSLSocket().getSocketFactory(); } httpsURLConnection.setSSLSocketFactory(noSSLv3Factory); httpsURLConnection.setHostnameVerifier(MY_DOMAIN_VERIFY); }else { httpsURLConnection.setSSLSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault()); httpsURLConnection.setHostnameVerifier(DO_NOT_VERIFY); }
核心是通过httpsURLConnection.setSSLSocketFactory使用自定义的校验逻辑。整体设计上使用策略模式决定采用哪种验证机制:
- makeContextWithCilentAndServer 单向验证方式(自定义信任的证书集合)
- makeContextWithServer 双向验证方式(自定义信任的证书集合,并使用客户端证书)
- makeContextToTrustAll (信任所有的CA证书,不安全,仅供测试阶段使用)
(2) 单向验证并自定义信任的证书集合
在App中,把服务端证书放到资源文件下(通常是asset目录下,因为证书对于每一个用户来说都是相同的,并且也不会经常发生改变),但是也可以放在设备的外部存储上。
public class SSLContextWithServer implements GetSSLSocket { // 在这里进行服务器正式的名称的配置 private String[] serverCertificateNames = {"serverCertificateNames1" ,"serverCertificateNames2"}; @Override public SSLContext getSSLSocket() { String[] caCertString = new String[serverCertificateNames.length]; for(int i = 0 ; i < serverCertificateNames.length ; i++) { try { caCertString[i] = readCaCert(serverCertificateNames[i]); } catch(Exception e) { } } SSLContext mSSLContext = null; try { mSSLContext = SSLContextFactory.getInstance().makeContextWithServer(caCertString); } catch(Exception e) { } return mSSLContext; }
serverCertificateNames中定义了App所信任的证书名称(这些证书文件必须要放在指定的文件路径下,并其要保证名称相同),而后就可以加载服务端证书链到keystore,通过获取到的可信任并带有服务端证书的keystore,就可以用它来初始化自定义的SSLContext了:
@Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { try { originalX509TrustManager.checkServerTrusted(chain, authType); } catch(CertificateException originalException) { try { X509Certificate[] reorderedChain = reorderCertificateChain(chain); CertPathValidator validator = CertPathValidator.getInstance("PKIX"); CertificateFactory factory = CertificateFactory.getInstance("X509"); CertPath certPath = factory.generateCertPath(Arrays.asList(reorderedChain)); PKIXParameters params = new PKIXParameters(trustStore); params.setRevocationEnabled(false); validator.validate(certPath, params); } catch(Exception ex) { throw originalException; } } }
(3) 跳过证书校验过程
和上面的过程类似,只不过这里提供的TrustManager不需要提供信任的证书集合,默认接受任意客户端证书即可:
public class AcceptAllTrustManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { //do nothing,接受任意客户端证书 } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { //do nothing,接受任意服务端证书 } @Override public X509Certificate[] getAcceptedIssuers() { return null; }
而后构造相应的SSLContext:
public SSLContext makeContextToTrustAll() throws Exception { AcceptAllTrustManager tm = new AcceptAllTrustManager(); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[] { tm }, null); return sslContext; }