本文為您提供Android端WebView情境下接入移動解析HTTPDNS Android SDK的最佳實務方案。
概述
Webview是Android系統提供的一個UI控制項,用來解析和顯示HTML+JS編寫的前端頁面。Android系統提供了API以實現WebView中的網路請求攔截與自訂邏輯注入。我們可以通過該API攔截WebView的各類網路請求,截取URL請求的Host,然後調用移動解析HTTPDNS Android SDK解析該Host,通過得到的IP組成新的URL來進行網路請求。本文旨在給出Android端在WebView的應用情境下接入移動解析HTTPDNSAndroid SDK的最佳實務供使用者參考。WebView在接入移動解析HTTPDNS Android SDK時,可應用於HTTP、HTTPS、SNI等情境,但需要滿足以下幾個前提條件:
Android SDK API Level > 21的裝置
HTTP請求報文頭不含Cookie的重新導向請求
Get請求
WebView情境下接入移動解析HTTPDNS Android SDK最佳實務完整代碼請參考Demo樣本工程源碼。
實踐方案
public void setWebViewClient(WebViewClient client);WebView提供了setWebViewClient介面對網路請求進行攔截,通過重載WebViewClient中的ShouldInterceptRequest方法,我們可以攔截到所有的網路請求:
public class WebViewClient{
// API < 21
public WebResourceResponse shouldInterceptRequest(WebView view,String url){
.....
}
// API >= 21
public WebResourceResponse shouldInterceptRequest(WebView view,WebResourceRequest request) {
.....
}
......
}Android SDK提供的shouldInterceptRequest方法在不同系統API下有不同版本。
當API<21時,shouldInterceptRequest的版本為:
public WebResourceResponse shouldInterceptRequest(WebView view,String url)此時僅能擷取到請求URL,要求方法、頭部資訊以及body等均無法擷取,強行攔截該請求可能無法得到正確響應。所以當API<21時,不對請求進行攔截:
public WebResourceResponse shouldInterceptRequest(WebView view,String url) {
return super.shouldInterceptRequest(view, url);
}當API≥21時,shouldInterceptRequest提供的方法為:
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String scheme = request.getUrl().getScheme().trim();
String method = request.getMethod();
Map<String, String> headerFields = request.getRequestHeaders();
// 只能正常處理不帶body的請求
if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))&& method.equalsIgnoreCase("get")) {
......
} else {
return super.shouldInterceptRequest(view, reqeust);
}
}由於WebResourceRequest並沒有提供請求body資訊,所以只能攔截GET請求,不能攔截POST請求。
方案實現
提供WebResourceResponse回調
webview攔截網路請求時,需要返回一個WebResourceResponse:
public WebResourceResponse(String mimeType, String encoding, InputStream data) ;建立WebResourceResponse對象需要提供:請求的MIME類型、請求的編碼、請求的輸入資料流。
其中請求的輸入資料流可以通過URLConnection.getInputStream()擷取,而MIME類型和encoding可以通過請求的ContentType擷取到,即通過URLConnection.getContentType ()獲得。
並不是所有的請求都能得到完整的contentType資訊,此時可以參考如下策略。
String contentType = conn.getContentType();
String mime = getMime(contentType);
String charset = getCharset(contentType);
// 不含有MIME類型的請求不攔截
if (TextUtils.isEmpty(mime)) {
return super.shouldInterceptRequest(view, request);
} else {
if (!TextUtils.isEmpty(charset)) {
// 如果同時擷取到MIME和charset可以直接攔截
return new WebResourceResponse(mime, charset, connection.getInputStream());
} else {
//擷取不到編碼資訊
// 二進位資源無需編碼資訊,可以進行攔截
if (isBinaryRes(mime)) {
Log.e(TAG, "binary resource for " + mime);
return new WebResourceResponse(mime, charset, connection.getInputStream());
}else {
// 非二進位資源需要編碼資訊,不攔截
Log.e(TAG, "non binary resource for " + mime);
return super.shouldInterceptRequest(view, request);
}
}
}佈建要求頭Host
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
......
URL url = new URL(request.getUrl().toString());
conn = (HttpURLConnection) url.openConnection();
//通過移動解析HTTPDNS Android SDK提供API獲得IP
String ip = null;
String[] ipv4Array = mDNSResolver.getIpv4ByHostFromCache(url.getHost(),true);
if (ipv4Array != null && ipv4Array.length > 0) {
ip = ipv4Array[0];
}
if (ip != null) {
//Log.d(TAG, "get IP: " + ip + " for host: " + url.getHost() + "from pdns resolver success!");
String newUrl = path.replaceFirst(url.getHost(), ip);
conn = (HttpURLConnection) new URL(newUrl).openConnection();
for (Map.Entry<String, String> field : headers.entrySet()) {
//設定Http請求的Head頭部資訊
conn.setRequestProperty(field.getKey(), field.getValue( ));
}
}
// conn.setRequestProperty("Host", url.getHost());
}
}情境案例
重新導向
通過攔截伺服器的Get請求,如果服務端的response包含重新導向,此時需要判斷原有請求中是否含有Cookie。如果原有要求標頭含Cookie,重新導向後Cookie會發生改變,這種情境下放棄攔截。如果原有要求標頭不包含Cookie,則需重新二次請求。
int code = conn.getResponseCode();
if (code >= 300 && code < 400) {
if (請求前序中含有cookie) {
// 不攔截
return super.shouldInterceptRequest(view, request);
}
//臨時重新導向和永久重新導向location的大小寫有區分
String location = conn.getHeaderField("Location");
if (location == null) {
location = conn.getHeaderField("location");
}
if (!(location.startsWith("http://") || location.startsWith("https://"))) {
//補全url
URL originalUrl = new URL(path);
location = originalUrl.getProtocol() + "://"+ originalUrl.getHost() + location;
}
Log.e(TAG, "code:" + code + "; location:" + location + ";path" + path);
發起二次請求
} else {
// redirect finish.
Log.e(TAG, "redirect finish");
......
}HTTPS認證校正
如果攔截到的請求是HTTPS請求,此時還需要進行認證校正:
if (conn instanceof HttpsURLConnection) {
final HttpsURLConnection httpsURLConnection = (HttpsURLConnection)conn;
//https情境認證校正
httpsURLConnection.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
String host = httpsURLConnection.getRequestProperty("Host");
if (null == host) {
host = httpsURLConnection.getURL().getHost();
}
return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
}
});
}HTTPS+SNI
如果HTTPS請求涉及到SNI情境,需要自訂SSLSocket,開發人員可以參考Android端HTTPS(含SNI)業務情境"IP直連"方案說明。
當前WebView接入移動解析HTTPDNS Android SDK最佳實務文檔只針對結合WebView情境下使用。
如何使用移動解析HTTPDNS Android SDK的網域名稱解析服務和接入移動解析HTTPDNS Android SDK的自身問題,請先查看Android SDK開發指南。
開發人員在WebView開發人員在情境下接入移動解析HTTPDNS Android SDK最佳實務完整代碼請參考Demo樣本工程源碼。