Android端WebView场景下接入阿里云公共DNS Android SDK最佳实践方案

本文档介绍了阿里云公共DNS Android SDKWebView场景下的接入方式。

概述

WebviewAndroid系统提供的一个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,开发者可以参考AndroidHTTPS(含SNI)业务场景"IP直连"方案说明

重要

  1. 当前WebView接入阿里云公共DNS Android SDK最佳实践文档只针对结合WebView场景下使用。

  2. 如何使用阿里云公共DNS Android SDK的域名解析服务和接入阿里云公共DNS Android SDK的自身问题,请先查看Android SDK开发指南。

  3. 开发者在WebView开发者在场景下接入阿里云公共DNS Android SDK最佳实践完整代码请参考Demo示例工程源码。