Android端HTTPDNS+Webview+OkHttp最佳实践

背景说明

阿里云HTTPDNS是避免DNS劫持的一种有效手段,在进行网络请求时,可以通过调用HTTPDNS提供的API来避免DNS劫持。

WebView加载的网页进行网络请求时,可以通过拦截WebView内网页发起的网络请求,由原生发起网络请求获取返回结果,再返回给网页,在原生发起网络请求的过程中,可以用HTTPDNS提供的域名解析代替系统的DNS解析,防止DNS劫持。

目前Android主流的网络请求库是OkHttp,所以本文通过OkHttp发起原生请求。

如果未集成OkHttp,可以通过HttpURLConnection发起网络请求,具体实现请参考AndroidHTTPDNS+Webview最佳实践

重要
  • 本文档为Android WebView场景下接入HTTPDNS的参考方案,提供的相关代码也为参考代码,非线上生产环境正式代码,建议您仔细阅读本文档,进行合理评估后再进行接入。

  • 由于Android生态碎片化严重,各厂商也进行了不同程度的定制,建议您灰度接入,并监控线上异常。如有问题欢迎您通过技术支持向我们反馈,方便我们及时优化。

  • 当前最佳实践文档只针对结合使用时,如何使用HTTPDNS解析出的IP,关于HTTPDNS本身的解析服务,请先查看Android SDK接入

代码示例

HTTPDNS+WebView+OkHttp最佳实践完整代码请参考WebView+HTTPDNS+OkHttp Android Demo

实现说明

第一步 OkHttp配置

1. OkHttp自定义DNS解析

OkHttp提供了接口Dns,可以让调用者自定义DNS解析。您可以在这个接口中集成HTTPDNS来实现自定义DNS解析,示例代码实现如下:

OkHttpClient.Builder()
        //自定义dns解析逻辑
        .dns(object : Dns {
            override fun lookup(hostname: String): List<InetAddress> {
                val inetAddresses = mutableListOf<InetAddress>()
                HttpDns.getService(requireContext(), "${accountID}")
                    .getHttpDnsResultForHostSync(hostname, RequestIpType.auto)?.apply {
                    if (!ipv6s.isNullOrEmpty()) {
                        for (i in ipv6s.indices) {
                            inetAddresses.addAll(
                                InetAddress.getAllByName(ipv6s[i]).toList()
                            )
                        }
                    } else if (!ips.isNullOrEmpty()) {
                        for (i in ips.indices) {
                            inetAddresses.addAll(
                                InetAddress.getAllByName(ips[i]).toList()
                            )
                        }
                    }
                }

                if (inetAddresses.isEmpty()) {
                    inetAddresses.addAll(Dns.SYSTEM.lookup(hostname))
                }
                return inetAddresses
            }
        })
        .build()
new OkHttpClient.Builder()
        //自定义dns解析逻辑实现
        .dns(new Dns() {
            @NonNull
            @Override
            public List<InetAddress> lookup(@NonNull String hostname) throws UnknownHostException {
                ArrayList<InetAddress> inetAddresses = new ArrayList<>();
                HTTPDNSResult result = HttpDns.getService(requireContext(), "${accountiD}")
                        .getHttpDnsResultForHostSync(hostname, RequestIpType.auto);
                if (result != null) {
                    processDnsResult(result, inetAddresses);
                }
                if (inetAddresses.isEmpty()) {
                    if (result.getIpv6s() != null && result.getIpv6s().length > 0) {
                        for (int i = 0; i < result.getIpv6s().length; i++) {
                            InetAddress[] ipV6InetAddresses = InetAddress.getAllByName(result.getIpv6s()[i]);
                            inetAddresses.addAll(Arrays.asList(ipV6InetAddresses));
                        }

                    } else if (result.getIps() != null && result.getIps().length > 0) {
                        for (int i = 0; i < result.getIps().length; i++) {
                            InetAddress[] ipV4InetAddresses = InetAddress.getAllByName(result.getIps()[i]);
                            inetAddresses.addAll(Arrays.asList(ipV4InetAddresses));
                        }
                    }
                }
                return inetAddresses;
            }
        })
        .build();
说明

建议在HTTPDNS域名解析失败的情况下,使用Local DNS作为域名解析的兜底逻辑。

2. OkHttp添加Cookie

WebView内部请求会自动携带当前domain下的Cookie,但是通过OkHttp进行网络请求时,默认不处理Cookie,需要手动处理Cookie逻辑。

OkHttp在获取OkHttpClient实例时提供了CookieJar接口来处理Cookie逻辑。示例代码实现如下:

OkHttpClient.Builder()
    .cookieJar(object: okhttp3.CookieJar {
        override fun loadForRequest(url: HttpUrl): List<Cookie> {
            val cookies = mutableListOf<Cookie>()
            val cookieStr =  CookieManager.getInstance().getCookie(url.toString())
            if (TextUtils.isEmpty(cookieStr)) {
                return cookies
            }
            cookieStr.split(";").forEach {
                Cookie.parse(url, it.trim())?.apply {
                    cookies.add(this)
                }
            }
            return cookies
        }

        override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
            cookies.forEach {
                CookieManager.getInstance().setCookie(url.toString(), "${it.name}=${it.value}")
            }
            CookieManager.getInstance().flush()
        }
    })
    .build()
OkHttpClient.Builder()
    .cookieJar(object: okhttp3.CookieJar {
        override fun loadForRequest(url: HttpUrl): List<Cookie> {
            val cookies = mutableListOf<Cookie>()
            val cookieStr =  CookieManager.getInstance().getCookie(url.toString())
            if (TextUtils.isEmpty(cookieStr)) {
                return cookies
            }
            cookieStr.split(";").forEach {
                Cookie.parse(url, it.trim())?.apply {
                    cookies.add(this)
                }
            }
            return cookies
        }

        override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
            cookies.forEach {
                CookieManager.getInstance().setCookie(url.toString(), "${it.name}=${it.value}")
            }
            CookieManager.getInstance().flush()
        }

    })
    .build()

第二步 WebView拦截网络请求

WebView提供了WebViewClient接口对网络请求进行拦截,通过重写WebViewClient中的shouldInterceptRequest()方法,我们可以拦截到所有的网络请求。示例代码实现如下:

webview.webViewClient = object : WebViewClient() {
    override fun shouldInterceptRequest(
        view: WebView?,
        request: WebResourceRequest?
    ): WebResourceResponse? {
        return super.shouldInterceptRequest(view, request)
    }
}
webview.setWebViewClient(new WebViewClient(){
    @Nullable
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        return super.shouldInterceptRequest(view, request);
    }
});

1. 能否拦截请求判断

  • 目前网络请求只拦截HTTP协议请求,其他协议不拦截,仍然由WebView自己处理;

  • 由于shouldInterceptRequest()的参数WebResourceRequest并没有提供网络请求的requestBody(请求体)信息,所以只能拦截GET请求;

判断是否可以拦截的示例代码实现如下:

private fun shouldIntercept(webResourceRequest: WebResourceRequest?): Boolean {
    if (webResourceRequest == null) {
        return false
    }
    val url = webResourceRequest.url ?: return false
    //非http协议不拦截
    if ("https" != url.scheme && "http" != url.scheme) {
        return false
    }
    //只拦截GET请求
    if ("GET".equals(webResourceRequest.method, true)) {
        return true
    }
    return false
}
private boolean shouldIntercept(WebResourceRequest request) {
    if (request == null || request.getUrl() == null) {
        return false;
    }
    //非http协议不拦截
    if (!"http".equals(request.getUrl().getScheme()) && !"https".equals(request.getUrl().getScheme())) {
        return false;
    }
    //只拦截GET请求
    if ("GET".equalsIgnoreCase(request.getMethod())) {
        return true;
    }

    return false;
}

2. 使用OkHttp进行网络请求

  • 使用OkHttp请求资源

  • 封装WebResourceResponse对象返回给WebView

示例代码实现如下:

private fun getResponseByOkHttp(webResourceRequest: WebResourceRequest?): WebResourceResponse? {
    if (webResourceRequest == null) {return null}
    try {
        val url = webResourceRequest.url.toString()
        val requestBuilder =
            Request.Builder().url(url).method(webResourceRequest.method, null)
        val requestHeaders = webResourceRequest.requestHeaders
        if (!requestHeaders.isNullOrEmpty()) {
            requestHeaders.forEach {
                requestBuilder.addHeader(it.key, it.value)
            }
        }
        val response = okHttpClient.newCall(requestBuilder.build()).execute()
        val code = response.code
        if (code != 200) {
            return null
        }
        val body = response.body
        if (body != null) {
            val contentType = body.contentType()
            val encoding = contentType?.charset()
            val mediaType = contentType?.toString()
            var mimeType = "text/plain"
            if (!TextUtils.isEmpty(mediaType)) {
                val mediaTypeElements = mediaType?.split(";")
                if (!mediaTypeElements.isNullOrEmpty()) {
                    mimeType = mediaTypeElements[0]
                }
            }
            val responseHeaders = mutableMapOf<String, String>()
            for (header in response.headers) {
                responseHeaders[header.first] = header.second
            }
            var message = response.message
            if (message.isBlank()) {
                message = "OK"
            }
            val resourceResponse =
                WebResourceResponse(mimeType, encoding?.name(), body.byteStream())
            resourceResponse.responseHeaders = responseHeaders
            resourceResponse.setStatusCodeAndReasonPhrase(code, message)
            return resourceResponse
        }
    } catch (e: Throwable) {
        e.printStackTrace()
    }
    return null
}
private WebResourceResponse getResponseByOkHttp(WebResourceRequest request) {
    try {
        String url = request.getUrl().toString();
        Request.Builder requestBuilder = new Request.Builder()
                .url(url)
                .method(request.getMethod(), null);
        Map<String, String> requestHeaders = request.getRequestHeaders();
        if (requestHeaders != null) {
            for (Map.Entry<String, String> entry : requestHeaders.entrySet()) {
                requestBuilder.addHeader(entry.getKey(), entry.getValue());
            }
        }
        Response response = okHttpClient.newCall(requestBuilder.build()).execute();
        if (200 != response.code()) {
            return null;
        }
        ResponseBody body = response.body();
        if (body != null) {
            MediaType contentType = body.contentType();
            if (contentType != null) {
                Charset encoding = contentType.charset();
                String mediaType = contentType.toString();
                String mimeType = "text/plain";
                if (!TextUtils.isEmpty(mediaType)) {
                    String[] mediaTypeElements = mediaType.split(";");
                    if (mediaTypeElements.length > 0) {
                       mimeType = mediaTypeElements[0];
                    }
                }
                Map<String, String> responseHeaders = new HashMap<>();
                for (String key : response.headers().names()) {
                    responseHeaders.put(key, response.header(key));
                }
                String message = response.message();
                if (TextUtils.isEmpty(message)) {
                    message = "OK";
                }
                WebResourceResponse resourceResponse = new WebResourceResponse(mimeType, encoding.name(), body.byteStream());
                resourceResponse.setResponseHeaders(responseHeaders);
                resourceResponse.setStatusCodeAndReasonPhrase(response.code(), message);
                return resourceResponse;
            }

        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}
说明

OkHttp内部封装了重定向逻辑,您不需要再处理重定向的场景。