- 背景
最近在忙一笔业务,是为了解决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是否优雅的问题,这里我没有深究。