背景说明
阿里云HTTPDNS是避免DNS劫持的一种有效手段,在进行网络请求时,可以通过调用HTTPDNS
提供的API来避免DNS劫持。
在WebView
加载的网页进行网络请求时,可以通过拦截WebView
内网页发起的网络请求,由原生发起网络请求获取返回结果,再返回给网页,在原生发起网络请求的过程中,可以用HTTPDNS提供的域名解析代替系统的DNS解析,防止DNS劫持。
目前Android主流的网络请求库是OkHttp
,所以本文通过OkHttp
发起原生请求。
如果未集成OkHttp
,可以通过HttpURLConnection
发起网络请求,具体实现请参考Android端HTTPDNS+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内部封装了重定向逻辑,您不需要再处理重定向的场景。