Android HttpDns OkHttp踩坑记录

  • 背景

最近在忙一笔业务,是为了解决dns污染的问题。

什么是dns污染就不细说了,接入后国内用户访问顺畅,但是国外用户请求接口提示『证书验证错误』

  • 原因

因为大家都懂的原因,国内网络环境复杂,所以国外用户访问国内服务器,我们引入了xx云的海外加速服务,也就是CDN。

首先我们先了解一下,xx云cdn加速的原理。

1.海外用户在访问国内环境时,会先访问到一个xx云的代理服务器;

2.这个服务器在解析请求时,会先去拿sni里的host值,得到对应证书,进行证书比对;

3.在单ip单证书下,只会返回一个证书,所以无所谓;

4.但是xx云代理服务器不会只服务一个厂家,所以存在单ip多证书的情况,正常情况下sni里的host值是你原始域名,也就返回对应的证书,因为httpDns会把域名换成ip,此时sni里的host值是个ip,则cdn代理服务器会返回默认证书,这个默认证书就可能与你的域名下证书不一样,导致证书验证失败。

  • 方案

所以解决方法就是

1.花钱让xx云的默认证书加上你们自己的证书。

2.把sni里的host值换回原始域名;

钱能解决的问题都不是问题,那么我们没钱的话就走方案2,替换回原始域名。

  • 具体步骤

如果你的网络框架是使用的原生方案,请参考以下帖子接入ssl。

Android支持Https总结

https://www.jianshu.com/p/2f6ace079568

如果你使用的网络框架是okhttp,请按照以下帖子接入X509TrustManager

添加okhttp的https

https://www.jianshu.com/p/98db08fcf70d

private X509TrustManager systemDefaultTrustManager() {
 try {
   TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
     TrustManagerFactory.getDefaultAlgorithm());
     trustManagerFactory.init((KeyStore) null);
     TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
     if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
     throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
     }
     return (X509TrustManager) trustManagers[0];
 } catch (GeneralSecurityException e) {
     throw new AssertionError(); // The system has no TLS. Just give up.
   }
 }

但是SSLSocketFactory请按照xx云的官方文档实现(腾讯云和阿里云的SSLSocketFactory代码是一样的)

这里要注意的是,我们看到xx云的SSLSocketFactory代码,其构造方法是为了最终去拿请求里的host,而我在实际中,只用了一个域名,所以我的构造方法就用了默认构造方法。

class SniSSLSocketFactory extends SSLSocketFactory {
private HttpsURLConnection conn;
​
//public SniSSLSocketFactory(HttpsURLConnection conn) {
//  this.conn = conn;
//}
​
@Override
public Socket createSocket() throws IOException {
 return null;
}
​
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
 return null;
}
​
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
 return null;
}
​
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
 return null;
}
​
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
 return null;
}
​
@Override
public String[] getDefaultCipherSuites() {
 return new String[0];
}
​
@Override
public String[] getSupportedCipherSuites() {
 return new String[0];
}
​
@Override
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
     //String mHost = this.conn.getRequestProperty("Host");
     String mHost = "你的实际host";
     if (mHost == null) {
     mHost = host;
     }
 Log.i("WGGetHostByName", "customized createSocket host is: " + mHost);
 InetAddress address = socket.getInetAddress();
 if (autoClose) {
 socket.close();
 }
 SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
 SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port);
 ssl.setEnabledProtocols(ssl.getSupportedProtocols());
​
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
 Log.i("WGGetHostByName", "Setting SNI hostname");
 sslSocketFactory.setHostname(ssl, mHost);
 } else {
 Log.d("WGGetHostByName", "No documented SNI support on Android <4.2, trying with reflection");
 try {
 java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);
 setHostnameMethod.invoke(ssl, mHost);
 } catch (Exception e) {
 Log.w("WGGetHostByName", "SNI not useable", e);
 }
 }
 // verify hostname and certificate
 SSLSession session = ssl.getSession();
 HostnameVerifier mHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
 if (!mHostnameVerifier.verify(mHost, session))
 throw new SSLPeerUnverifiedException("Cannot verify hostname: " + mHost);
 Log.i("WGGetHostByName",
 "Established " + session.getProtocol() + " connection with " + session.getPeerHost() + " using " + session.getCipherSuite());
 return ssl;
}
}

*结尾

另外,有一个讨论性的文章

Android端打开HttpDns的正确姿势

https://www.jianshu.com/p/b0c154215b48

讨论了使用SSLSocketFactory是否优雅的问题,这里我没有深究。

你可能感兴趣的:(Android HttpDns OkHttp踩坑记录)