1. 背景说明
阿里云 HTTPDNS 是一种有效避免 DNS 劫持的手段。在进行网络请求时,可以通过调用 HTTPDNS 提供的 API 来绕过系统默认的 DNS 解析,从而减少或杜绝 DNS 劫持风险。
在 WebView 加载的网页中,当网页发起网络请求时,我们可以通过拦截 WebView 内的请求,让原生层(App)来执行实际的网络请求。在原生层发起网络请求的过程中,即可使用 HTTPDNS 提供的解析结果,替换系统 DNS 解析,以防止 DNS 劫持。
目前 Android 端主流的网络请求库是 OkHttp,因此本文主要基于 OkHttp 的实现。如果未集成 OkHttp,也可使用 HttpURLConnection 或其他网络库,但相对来说,基于 OkHttp 的实现更简洁优雅。更多实现细节可参考Android端HTTPDNS+Webview+HttpURLConnection最佳实践
本文档为Android WebView场景下接入HTTPDNS的参考方案,提供的相关代码也为参考代码,非线上生产环境直接可用的正式代码,建议您仔细阅读本文档,进行合理评估后再进行接入。
由于Android生态碎片化严重,各厂商也进行了不同程度的定制,建议您灰度接入,并监控线上异常。如有问题欢迎您通过 技术支持向我们反馈,方便我们及时优化。
当前文档只针对“在 WebView 场景下如何结合 HTTPDNS 解析出的 IP 进行请求”做说明,若需了解 HTTPDNS 本身的解析服务,请先查看Android SDK接入。
2. WebView只拦截GET请求的限制及影响
在 Android 的 WebView 中,通过 WebViewClient.shouldInterceptRequest()
方法,我们只能获取到 GET 请求的详细信息。对于 POST 等其他请求方式,body
并不提供给开发者,因此无法全面拦截并替换为 HTTPDNS 解析。
然而,这并不代表接入 HTTPDNS 毫无意义,主要原因如下:
绝大部分网页静态资源均使用 GET 请求 一般网页所需的静态资源(例如图片、脚本、样式表等)几乎全部通过 GET 请求获取。通过拦截这些 GET 请求并使用 HTTPDNS,可以覆盖主要的资源加载过程,大幅降低被劫持的风险。
API 调用中也有相当比例使用 GET 不少接口(尤其是查询类或可被缓存的接口)会使用 GET 请求。因此拦截这些请求,同样可通过 HTTPDNS 避免 DNS 劫持。
资源多托管在 CDN,HTTPDNS 提升就近调度 大多数静态资源通常分发在 CDN 节点上,CDN 的就近接入能力依赖域名解析的准确性。通过 HTTPDNS,可以绕过系统 DNS 劫持,确保域名解析指向最优 CDN 节点,让用户获得更好的访问速度和体验。
只拦截 GET 请求也能规避绝大部分劫持场景 DNS 劫持往往针对热门域名或海量静态资源域名,而这些域名的访问几乎都是 GET 请求。即使目前只能在 GET 请求上使用 HTTPDNS,也能覆盖绝大多数流量,实现 80% 甚至更高比例的防护。
综上所述,虽然 WebView 层面目前只能拦截并改写 GET 请求,但在实际业务中,这种方式通常已足以涵盖主要流量,并在 CDN 场景下实现更精准的就近调度,从而在安全和性能上都发挥显著的提升作用。
3. Demo代码示例
HTTPDNS+WebView+OkHttp
最佳实践完整代码请参考WebView+HTTPDNS+OkHttp Android Demo。
4. 实现说明
以下步骤展示如何基于 OkHttp 和 WebView 进行 HTTPDNS 接入,并在原生层拦截并处理网页中发起的请求。
4.1 OkHttp配置
4.1.1 自定义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(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(accountiD)
.getHttpDnsResultForHostSync(hostname, RequestIpType.auto);
if (result != null) {
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));
}
}
}
if (inetAddresses.isEmpty()) {
inetAddresses.addAll(Dns.SYSTEM.lookup(hostname));
}
return inetAddresses;
}
})
.build();
建议在HTTPDNS域名解析失败的情况下,使用Local DNS作为域名解析的兜底逻辑。
4.1.2 添加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()
4.2 WebView拦截网络请求
4.2.1 实现WebViewClient
WebView
提供了 WebViewClient
接口,重写 shouldInterceptRequest()
方法即可拦截网页的资源加载请求。通过此方法,可将网页发起的请求改为使用 原生层 (OkHttp) 来请求数据,然后再将结果返回给 WebView。示例代码实现如下:
webview.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
if (shouldIntercept(request)) {
return getResponseByOkHttp(request)
}
return super.shouldInterceptRequest(view, request)
}
}
webview.setWebViewClient(new WebViewClient(){
@Nullable
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
if (shouldIntercept(request)) {
return getResponseByOkHttp(request);
}
return super.shouldInterceptRequest(view, request);
}
});
4.2.2 判断能否拦截请求
在 shouldIntercept()
方法中,只有以下情况才做拦截:
只拦截
http
或https
协议的请求。其他协议不拦截,仍然由WebView
自己处理.只拦截
GET
请求(原因详见上文:“WebView只拦截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;
}
4.2.3 使用OkHttp进行网络请求
使用 OkHttp 根据请求 URL、Header 等信息发起网络请求。
将响应结果通过
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 已经封装了重定向逻辑,开发者无需在拦截层显式处理 3XX 等重定向场景。
5. 总结
HTTPDNS 可以有效降低 DNS 劫持风险,在 WebView 中通过拦截请求并使用原生网络库(OkHttp)再结合 HTTPDNS,能显著提升域名解析的安全性。
因 WebView 机制所限,只能在 GET 请求上实现拦截,但这对于加载静态资源、常用 GET 接口等已覆盖大部分场景。
强烈建议 开发者在实际项目中灰度接入,关注线上异常与适配情况,并根据业务需要做进一步优化或定制化处理。
如有更多疑问或需要技术支持,欢迎通过 技术支持 反馈给我们。我们将持续优化 HTTPDNS 的接入体验和兼容性。
- 本页导读 (0)
- 1. 背景说明
- 2. WebView只拦截GET请求的限制及影响
- 3. Demo代码示例
- 4. 实现说明
- 4.1 OkHttp配置
- 4.2 WebView拦截网络请求
- 5. 总结