本文档介绍了阿里云公共DNS Android SDK在WebView场景下的接入方式。
概述
Webview是Android系统提供的一个UI控件,用来解析和显示HTML+JS编写的前端页面。Android系统提供了API以实现WebView中的网络请求拦截与自定义逻辑注入。我们可以通过该API拦截WebView的各类网络请求,截取URL请求的Host,然后调用阿里云公共DNS Android SDK解析该Host,通过得到的IP组成新的URL来进行网络请求。本文旨在给出Android端在WebView的应用场景下接入阿里云公共DNS Android SDK的最佳实践供用户参考。WebView在接入阿里云公共DNS Android SDK时,可应用于HTTP、HTTPS、SNI等场景,但需要满足以下几个前提条件:
Android SDK API Level>21的设备
HTTP请求报文头不含Cookie的重定向请求
Get请求
WebView场景下接入阿里云公共DNS 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 ()
获得。
text/html;charset=utf-8
并不是所有的请求都能得到完整的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();
//通过阿里云公共DNS 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());
}
}
接入的场景
1.重定向
通过拦截服务器的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");
......
}
2. 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);
}
});
}
3. HTTPS+SNI
如果HTTPS请求涉及到SNI场景,需要自定义SSLSocket,开发者可以参考Android端HTTPS(含SNI)业务场景"IP直连"方案说明
当前WebView接入阿里云公共DNS Android SDK最佳实践文档只针对结合WebView场景下使用。
如何使用阿里云公共DNS Android SDK的域名解析服务和接入阿里云公共DNS Android SDK的自身问题,请先查看Android SDK开发指南。
开发者在WebView开发者在场景下接入阿里云公共DNS Android SDK最佳实践完整代码请参考Demo示例工程源码。