Android使用HTTPS进行IP直连握手失败问题(okHttp)
为什么要使用ip直连这种方式去请求我们的服务器呢?这其实和国内运营伤有关,运营商有时为了利益会将你的域名劫持换成他人的域名,为了防止这种情况的发生通用的解决办法要么联系运营商要么就只能使用ip直连了。普遍大家目前使用的都是okHttp,这里就以okHttp为例子。其实非常简单只需要设置一下两个方法就行:
OkHttpClient.Builder builder = new OkHttpClient.Builder(); .... String domain = ....; builder.sslSocketFactory(new TlsSniSocketFactory(domain), new SSLUtil.TrustAllManager()) .hostnameVerifier(new TrueHostnameVerifier(domain));
通过调用sslSocketFactory()方法传入两个参数一个是:SSLSocket,还有一个x509TrustManager。我们来看看第一个参数是如何实现的:
public class TlsSniSocketFactory extends SSLSocketFactory { private final String TAG = TlsSniSocketFactory.class.getSimpleName(); HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); String peerHost; public TlsSniSocketFactory(String peerHost) { this.peerHost = peerHost; } public TlsSniSocketFactory() { } @Override public Socket createSocket() { return null; } @Override public Socket createSocket(String host, int port) { return null; } @Override public Socket createSocket(String host, int port, InetAddress localHost, int localPort) { return null; } @Override public Socket createSocket(InetAddress host, int port) { return null; } @Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) { return null; } // TLS layer @Override public String[] getDefaultCipherSuites() { return new String[0]; } @Override public String[] getSupportedCipherSuites() { return new String[0]; } @Override public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException { if (TextUtils.isEmpty(peerHost)) { peerHost = host; peerHost = ....; } Log.i(TAG, "customized createSocket. host: " + peerHost); InetAddress address = plainSocket.getInetAddress(); if (autoClose) { // we don't need the plainSocket plainSocket.close(); } // create and connect SSL socket, but don't do hostname/certificate verification yet SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0); SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port); // enable TLSv1.1/1.2 if available ssl.setEnabledProtocols(ssl.getSupportedProtocols()); // set up SNI before the handshake if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { Log.i(TAG, "Setting SNI hostname"); sslSocketFactory.setHostname(ssl, peerHost); } else { Log.d(TAG, "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, peerHost); } catch (Exception e) { Log.w(TAG, "SNI not useable", e); } } // verify hostname and certificate SSLSession session = ssl.getSession(); if (!hostnameVerifier.verify(peerHost, session)) throw new SSLPeerUnverifiedException("Cannot verify hostname: " + peerHost); Log.i(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() + " using " + session.getCipherSuite()); return ssl; } }
为了防止获取不到domain将外围的domain塞入,将这个domain塞入返回我们的ssl。x509TrustManagerx信任了所有的证书,当然正常情况下应该使用和后端约定好的证书,代码如下:
public class SSLUtil { /** * 默认信任所有的证书 * TODO 最好加上证书认证,主流App都有自己的证书 * * @return */ @SuppressLint("TrulyRandom") public static SSLSocketFactory createSSLSocketFactory() { SSLSocketFactory sSLSocketFactory = null; try { SSLContext sc = SSLContext.getInstance("TLS"); sc.init(null, new TrustManager[]{new TrustAllManager()}, new SecureRandom()); sSLSocketFactory = sc.getSocketFactory(); } catch (Exception e) { } return sSLSocketFactory; } public static class TrustAllManager implements X509TrustManager { @Override public void checkServerTrusted(X509Certificate[] chain, String authType) { } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) { } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } } public static class TrustAllHostnameVerifier implements HostnameVerifier { @Override public boolean verify(String hostname, SSLSession session) { return true; } } }
最后https需要校验我的domain是否是服务器提供的domain:
public class TrueHostnameVerifier implements HostnameVerifier { public String domain; public TrueHostnameVerifier(String domain) { this.domain = domain; } public TrueHostnameVerifier() { } @Override public boolean verify(String hostname, SSLSession session) { if(TextUtils.isEmpty(domain)) { domain = ...; } return HttpsURLConnection.getDefaultHostnameVerifier().verify(domain, session); } }
以上代码可以直接拷贝,省略号代码具体代码可能需要你自己去实现。希望对你有所帮助。