博客主页java
本文主要介绍HTTPS(含SNI)业务场景下在Android端实现“IP直连”的通用解决方案。若是以OkHttp做为网络开发框架,因为OkHttp提供了自定义DNS服务接口能够优雅地实现IP直连。其方案相比通用方案更加简单且通用性更强,推荐您参考HttpDns+OkHttp最佳实践接入HttpDns。算法
发送HTTPS请求首先要进行SSL/TLS握手,握手过程大体以下:segmentfault
上述过程当中,和HTTPDNS有关的是第3步,客户端须要验证服务端下发的证书,验证过程有如下两个要点:浏览器
若是上述两点都校验经过,就证实当前的服务端是可信任的,不然就是不可信任,应当中断当前链接。服务器
当客户端使用HTTPDNS解析域名时,请求URL中的host会被替换成HTTPDNS解析出来的IP,因此在证书验证的第2步,会出现domain不匹配的状况,致使SSL/TLS握手不成功。网络
SNI(Server Name Indication)是为了解决一个服务器使用多个域名和证书的SSL/TLS扩展。它的工做原理以下:session
目前,大多数操做系统和浏览器都已经很好地支持SNI扩展,OpenSSL 0.9.8也已经内置这一功能。并发
上述过程当中,当客户端使用HTTPDNS解析域名时,请求URL中的host会被替换成HTTPDNS解析出来的IP,致使服务器获取到的域名为解析后的IP,没法找到匹配的证书,只能返回默认的证书或者不返回,因此会出现SSL/TLS握手不成功的错误。app
好比当你须要经过HTTPS访问CDN资源时,CDN的站点每每服务了不少的域名,因此须要经过SNI指定具体的域名证书进行通讯。
针对“domain不匹配”问题,能够采用以下方案解决:hook证书校验过程当中第2步,将IP直接替换成原来的域名,再执行证书验证。框架
【注意】基于该方案发起网络请求,若报出SSL校验错误,Android系统报错System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.,请检查应用场景是否为SNI(单IP多HTTPS域名)。
此示例针对HttpURLConnection接口。
try { String url = "https://140.205.160.59/?sprefer=sypc00"; HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); connection.setRequestProperty("Host", "m.taobao.com"); connection.setHostnameVerifier(new HostnameVerifier() { /* * 关于这个接口的说明,官方有文档描述: * This is an extended verification option that implementers can provide. * It is to be used during a handshake if the URL's hostname does not match the * peer's identification hostname. * * 使用HTTPDNS后URL里设置的hostname不是远程的主机名(如:m.taobao.com),与证书颁发的域不匹配, * Android HttpsURLConnection提供了回调接口让用户来处理这种定制化场景。 * 在确认HTTPDNS返回的源站IP与Session携带的IP信息一致后,您能够在回调方法中将待验证域名替换为原来的真实域名进行验证。 * */ @Override public boolean verify(String hostname, SSLSession session) { return HttpsURLConnection.getDefaultHostnameVerifier().verify("m.taobao.com", session); return false; } }); connection.connect(); } catch (Exception e) { e.printStackTrace(); } finally { }
定制SSLSocketFactory,在createSocket时替换为HTTPDNS的IP,并进行SNI/HostNameVerify配置。
public class HttpDnsTLSSniSocketFactory extends SSLSocketFactory { private final String TAG = HttpDnsTLSSniSocketFactory.class.getSimpleName(); HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); private HttpsURLConnection conn; public HttpDnsTLSSniSocketFactory(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; } // 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 { String peerHost = this.conn.getRequestProperty("Host"); if (peerHost == null) peerHost = host; 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; } }
对于须要设置SNI的站点,一般须要重定向请求,下面给出了重定向请求的处理方法。
public void recursiveRequest(String path, String reffer) { URL url = null; try { url = new URL(path); conn = (HttpsURLConnection) url.openConnection(); String ip = httpdns.getIpByHostAsync(url.getHost()); if (ip != null) { // 经过HTTPDNS获取IP成功,进行URL替换和HOST头设置 Log.d(TAG, "Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!"); String newUrl = path.replaceFirst(url.getHost(), ip); conn = (HttpsURLConnection) new URL(newUrl).openConnection(); // 设置HTTP请求头Host域 conn.setRequestProperty("Host", url.getHost()); } conn.setConnectTimeout(30000); conn.setReadTimeout(30000); conn.setInstanceFollowRedirects(false); HttpDnsTLSSniSocketFactory sslSocketFactory = new HttpDnsTLSSniSocketFactory(conn); conn.setSSLSocketFactory(sslSocketFactory); conn.setHostnameVerifier(new HostnameVerifier() { /* * 关于这个接口的说明,官方有文档描述: * This is an extended verification option that implementers can provide. * It is to be used during a handshake if the URL's hostname does not match the * peer's identification hostname. * * 使用HTTPDNS后URL里设置的hostname不是远程的主机名(如:m.taobao.com),与证书颁发的域不匹配, * Android HttpsURLConnection提供了回调接口让用户来处理这种定制化场景。 * 在确认HTTPDNS返回的源站IP与Session携带的IP信息一致后,您能够在回调方法中将待验证域名替换为原来的真实域名进行验证。 * */ @Override public boolean verify(String hostname, SSLSession session) { String host = conn.getRequestProperty("Host"); if (null == host) { host = conn.getURL().getHost(); } return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session); } }); int code = conn.getResponseCode();// Network block if (needRedirect(code)) { //临时重定向和永久重定向location的大小写有区分 String location = conn.getHeaderField("Location"); if (location == null) { location = conn.getHeaderField("location"); } if (!(location.startsWith("http://") || location .startsWith("https://"))) { //某些时候会省略host,只返回后面的path,因此须要补全url URL originalUrl = new URL(path); location = originalUrl.getProtocol() + "://" + originalUrl.getHost() + location; } recursiveRequest(location, path); } else { // redirect finish. DataInputStream dis = new DataInputStream(conn.getInputStream()); int len; byte[] buff = new byte[4096]; StringBuilder response = new StringBuilder(); while ((len = dis.read(buff)) != -1) { response.append(new String(buff, 0, len)); } Log.d(TAG, "Response: " + response.toString()); } } catch (MalformedURLException e) { Log.w(TAG, "recursiveRequest MalformedURLException"); } catch (IOException e) { Log.w(TAG, "recursiveRequest IOException"); } catch (Exception e) { Log.w(TAG, "unknow exception"); } finally { if (conn != null) { conn.disconnect(); } } } private boolean needRedirect(int code) { return code >= 300 && code < 400; }
若是个人文章对您有帮助,不妨点个赞鼓励一下(^_^)