本文檔介紹Android端HTTPS(含SNI)業務情境“IP直連”的通用解決方案。
概述
HTTPS是一種通過電腦網路進行安全通訊的傳輸協議,經由HTTP進行通訊,利用SSL/TLS建立全通道,加密資料包。HTTPS使用的主要目的是提供對網站伺服器的身份認證,同時保護交換資料的隱私與完整性,TLS是傳輸層加密協議,前身是SSL協議。HTTPS下有兩種業務情境普通情境和SNI情境,SNI(Server Name Indication)是用來改善伺服器與用戶端SSL(Secure Socket Layer)和TLS(Transport Layer Security)的擴充,主要解決一台伺服器能夠提供多個網域名稱服務 (DNS)的情況。
普通情境
普通情境使用“IP直連”對開發人員來說很方便,直接將請求URL中的Host替換成IP,在執行認證驗證時將IP再替換成原來的網域名稱即可。
SNI情境
SNI(單IP多HTTPS認證)情境下,針對HttpsURLConnection介面,開發人員需要定製SSLSocketFactory,在createSocket時通過移動解析HTTPDNS Android SDK提供的API替換為網域名稱解析後的IP後,並需要對 SNI/HostNameVerify進行配置。
使用者如果使用ipv6的IP地址進行替換請求URL中的Host,需要對ipv6地址加上“[]”。
例如:https://[2400:3200:xxx:1]/path
由於OKHttp提供了自訂DNS服務介面可以優雅地實現IP直連。如果您是Android開發人員,並且以OKHttp作為網路開發架構。其方案相比通用方案更加簡單且通用性更強,推薦您參考Android端OkHttp架構接入HTTPDNS Android SDK最佳實務方案。
如果使用者在Server端使用了CDN服務或者未來會使用CDN服務,請參考HTTPS下SNI情境的解決方案使用。
實踐方案
HTTPS下普通情境解決方案
針對“domain不匹配”的問題,可以採用hook認證校正的解決方案,將IP直接替換成原來的網域名稱,再執行證 書驗證。
此樣本針對HttpURLConnection
try {
//使用者如果使用ipv6的IP地址進行替換請求URL中的Host,需要對ipv6地址加上“[]”。
//例如:https://[2400:3200:xxx:1]/path
String url = "https://124.239.XX.XX/?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 Android SDK後URL裡設定的hostname不是遠端主機名稱(如:m.taobao.com),與憑證發行的域不匹配,
* Android HttpsURLConnection提供了回調介面讓使用者來處理這種定製化情境。
* 在確認移動解析HTTPDNS Android SDK返回的來源站點IP與Session攜帶的IP資訊一致後,您可以在回調方法中將待驗證網域名稱替換為原來的真實網域名稱進行驗證。
*
*/
@Override
public boolean verify(String hostname, SSLSession session) {
return HttpsURLConnection.getDefaultHostnameVerifier().verify("m.taobao.com", session);
}
});
connection.connect();
} catch (Exception e) {
e.printStackTrace();
} finally {
}基於該方案發起網路請求,若報出SSL校正錯誤,比如Android系統報錯System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.,請檢查應用情境是否為SNI(單IP多HTTPS網域名稱)。
HTTPS下SNI情境解決方案
針對HttpsURLConnection介面提供了在SNI情境使用的範例程式碼,完整代碼請參考Demo樣本工程源碼。
定製SSLSocketFactory,在建立createSocket時將網域名稱替換成網域名稱解析後的IP,並進行SNI/ HostNameVerfy配置。
class TlsSniSocketFactory extends SSLSocketFactory {
private final String TAG = TlsSniSocketFactory.class.getSimpleName();
HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
private HttpsURLConnection conn;
public TlsSniSocketFactory(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 = dnsResolver.getIPV4ByHost(url.getHost());
//使用者如果使用ipv6的IP地址進行替換請求URL中的Host,需要對ipv6地址加上“[]”。
//例如:https://[2400:3200:xxx:1]/path
if (ip != null) {
// 通過移動解析HTTPDNS Android SDK擷取IP成功,進行URL替換和HOST頭設定
Log.d(TAG, "get IP: " + ip + " for host: " + url.getHost() + "from pdns resolver success!");
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);
TlsSniSocketFactory sslSocketFactory = new TlsSniSocketFactory(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 Android SDK後URL裡設定的hostname不是遠端主機名稱(如:m.taobao.com),與憑證發行的域不匹配,
* Android HttpsURLConnection提供了回調介面讓使用者來處理這種定製化情境。
* 在確認移動解析HTTPDNS Android SDK返回的來源站點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();
}
}
}當前接入移動解析HTTPDNS Android SDK文檔只針對結合HTTPS的普通情境和SNI情境下使用。
如何使用移動解析HTTPDNS Android SDK的網域名稱解析服務和接入移動解析HTTPDNS Android SDK的自身問題,請先查看Android SDK開發指南。
開發人員在HTTPS下SNI情境接入移動解析HTTPDNS Android SDK完整代碼請參考Demo樣本工程源碼。