Android端HTTPDNS+Webview+本地代理最佳实践

本文档介绍在Android客户端上使用Webview接入HTTPDNS的方案。

1. 前言

通过Android SDK接入,您已经了解如何在Android平台的Native场景中使用HTTPDNS,实现防劫持、调度精准、解析及时生效的能力。而Android上还有一个频繁发生网络请求的场景:WebView。

WebViewAndroid系统提供的网页显示组件,用于在应用中嵌入网页内容。期望在WebView加载网页时,也能使用HTTPDNS,提升网络安全性与网络性能。

本文档针对Android WebView场景下如何使用HTTPDNS解析出的IP进行网络请求,关于HTTPDNS本身的解析服务,请先查看上述SDK接入文档。

2. 技术现状与挑战

2.1 Android WebView的网络请求限制

在 Android 中,WebView 依赖系统内置的浏览器内核(基于 Chromium)加载网页,其网络请求链路与应用自身的网络库完全独立,域名解析也走内核自带的 DNS 流程,很多请求无法在 Java 层直接拦截,这种封闭性为接入 HTTPDNS 带来了天然限制。

2.2 技术方案演进

随着Android系统和WebView的不断发展,WebView集成HTTPDNS的技术方案也在持续演进:

拦截方案

在 shouldInterceptRequest() 回调中拦截资源请求,交由应用内网络库(如 OkHttp)重新发起,并在其中集成 HTTPDNS 解析,从而绕过系统 DNS。

该方法实现简单,对部分 GET 请求资源和接口可生效,但存在明显局限:只能获取 GET 请求的完整信息,POST 等方法的请求体不会暴露,因此对 WebView 的整体流量覆盖有限。

代理方案(推荐)

在本地启动代理服务器,使用 AndroidX WebKit 的 ProxyController 为 WebView 配置统一的 HTTP/HTTPS/WebSocket 代理。这样可以将内核的所有流量透明转发到应用可控的网络栈中,实现对所有协议和请求方法的拦截与 HTTPDNS 解析,避免了 shouldInterceptRequest() 的覆盖率瓶颈。

3. 本地代理方案(推荐方案)

3.1 方案概述

本方案通过在应用内启动一个本地HTTP代理服务器,使用AndroidX WebKitProxyControllerWebView的所有网络请求路由到本地代理。在代理层面实现HTTPDNS域名解析,然后将请求转发到真实服务器。

工作流程

安卓代理结构图

3.2 依赖集成

在 app/build.gradle 中添加依赖:

// 依赖版本需要按实际情况调整
dependencies {
    implementation 'androidx.webkit:webkit:1.7.0'
    implementation 'com.aliyun.ams:alicloud-android-httpdns:2.6.5'
    implementation 'io.github.littleproxy:littleproxy:2.4.4'
    implementation 'io.netty:netty-all:4.1.42.Final'
    implementation 'com.google.guava:guava:30.1.1-android'
    implementation 'org.slf4j:slf4j-android:1.7.30'
}

3.3 接入步骤

集成HTTPDNS WebView代理需要3个核心步骤:

步骤1:初始化HTTPDNS

Application中初始化HTTPDNS服务:

public class WebviewProxyApplication extends Application {
    public static String accountId = "your_account_id";
    public static String secretKey = "your_secret_key"; // 可选
    @Override
    public void onCreate() {
        super.onCreate();
        initHttpDns();
    }
    
    private void initHttpDns() {
        try {
            InitConfig config = new InitConfig.Builder()
                .setContext(this)
                .setTimeout(5000)
                .setEnableCacheIp(true)
                .setEnableExpiredIp(true)
                .setSecretKey(secretKey) // 可选,用于鉴权
                .build();
            HttpDns.init(accountId, config);
            Log.i("HTTPDNS", "初始化成功");
        } catch (Exception e) {
            Log.e("HTTPDNS", "初始化失败", e);
        }
    }
}

步骤2:启动LittleProxy代理服务

创建ProxyService管理代理服务器:

public class ProxyService extends Service {
    private HttpProxyServer proxyServer;
    private HttpDnsResolver httpDnsResolver;
    private boolean isRunning = false;
    
    public boolean startProxyServer() {
        if (isRunning) return true;
        
        try {
            // 创建HTTPDNS解析器
            httpDnsResolver = new HttpDnsResolver(this);
            
            // 启动LittleProxy服务器
            int port = generateRandomPort();
            proxyServer = DefaultHttpProxyServer.bootstrap()
                .withPort(port)
                .withAddress(new InetSocketAddress("127.0.0.1", port))
                .withServerResolver(httpDnsResolver) // 使用HTTPDNS解析器
                .withConnectTimeout(10000)
                .withIdleConnectionTimeout(30000)
                .start();
                
            isRunning = true;
            Log.i("Proxy", "代理服务器启动成功: 127.0.0.1:" + port);
            return true;
        } catch (Exception e) {
            Log.e("Proxy", "代理服务器启动失败", e);
            return false;
        }
    }
    
    public void stopProxyServer() {
        if (proxyServer != null) {
            proxyServer.stop();
            isRunning = false;
            Log.i("Proxy", "代理服务器已停止");
        }
    }
    
    public boolean isProxyRunning() {
        return isRunning;
    }
}

// HTTPDNS解析器实现
class HttpDnsResolver implements HostResolver {
    private HttpDnsService httpDnsService;
    
    public HttpDnsResolver(Context context) {
        httpDnsService = HttpDns.getService(WebviewProxyApplication.accountId);
    }
    
    @Override
    public InetSocketAddress resolve(String host, int port) throws UnknownHostException {
        try {
            // 使用HTTPDNS解析
            HTTPDNSResult result = httpDnsService.getHttpDnsResultForHostSyncNonBlocking(
                host, RequestIpType.auto);
            
            if (result != null && result.getIps() != null && result.getIps().length > 0) {
                String ip = result.getIps()[0];
                Log.d("DNS", "HTTPDNS解析: " + host + " -> " + ip);
                return new InetSocketAddress(ip, port);
            }
            
            // 降级到系统DNS
            return new InetSocketAddress(InetAddress.getByName(host), port);
        } catch (Exception e) {
            Log.w("DNS", "解析失败,使用系统DNS: " + host);
            return new InetSocketAddress(InetAddress.getByName(host), port);
        }
    }
}

步骤3:配置WebView代理

Activity中配置WebView使用本地代理:

public class MainActivity extends AppCompatActivity {
    private WebView webView;
    private ProxyService proxyService;
    private boolean isProxyRunning = false;
    
    // 主线程Executor用于ProxyController
    private final Executor mainExecutor = ContextCompat.getMainExecutor(this);
    
    private void configureWebViewProxy() {
        // 检查系统是否支持代理配置
        if (!WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
            Log.w("WebView", "当前系统不支持代理配置");
            return;
        }
        
        if (isProxyRunning) {
            String proxyAddress = proxyManager.getProxyAddress();
        
            // 配置WebView使用本地代理
            ProxyConfig proxyConfig = new ProxyConfig.Builder()
                .addProxyRule(proxyAddress, ProxyConfig.MATCH_HTTP)
                .addProxyRule(proxyAddress, ProxyConfig.MATCH_HTTPS)
                .addDirect(ProxyConfig.MATCH_ALL_SCHEMES) // 其他协议直连
                .build();
                
            ProxyController.getInstance().setProxyOverride(proxyConfig, mainExecutor, () -> {
                Log.i("WebView", "代理配置成功");
            });
        } else {
            // 清除代理配置
            ProxyController.getInstance().clearProxyOverride(mainExecutor, () -> {
                Log.i("WebView", "代理配置已清除");
            });
        }
    }
    
    private void loadUrl(String url) {
        // 确保代理配置生效后再加载URL
        configureWebViewProxy();
        webView.loadUrl(url);
    }
}

3.4 完整实现参考

考虑创建本地代理服务、集成HTTPDNS、配置Webview等步骤有一定实现成本,我们在GitHub上开源了一份完整实现:HTTPDNS Webview Demo

核心组件包括:

  • HttpDnsResolver: 实现LittleProxyHostResolver接口,集成HTTPDNS解析

  • ProxyService: Android Service管理代理服务器生命周期

  • ProxyManager: 代理状态管理和服务绑定

  • MainActivity: UI交互和WebView代理配置

方案内置多层降级机制确保稳定性:系统版本不支持时自动跳过ProxyController配置,HTTPDNS解析失败时自动降级到系统DNS,解析得到IP不可用时自动切换等。

4. 传统拦截方案

对于不支持ProxyController的旧版本系统(WebView < 72),可以使用传统的拦截方案作为降级策略。

4.1 方案概述

传统拦截方案通过重写WebViewClient.shouldInterceptRequest()方法,拦截WebView的网络请求,使用集成了HTTPDNSOkHttp客户端重新发起请求。

主要特点:

  • 仅支持GET请求拦截

  • 需要手动处理Cookie

  • 实现复杂度较高,维护成本大

  • 兼容性好,支持较低版本Webview

4.2 详细实现

由于传统拦截方案涉及较多技术细节和边缘情况处理,完整的实现代码和使用说明请参考:AndroidHTTPDNS+OkHttp最佳实践

5. 方案对比与总结

维度 / 方案

代理方案

拦截方案

官方支持度

GoogleAndroidX WebKit中引入的正式API,完全公开、长期可用

使用公开WebViewClient API,但功能受限于GET请求

生效版本

Android 5.0+

WebView 72+

Android 5.0+

协议覆盖

HTTP/HTTPS/WebSocket

仅限HTTP/HTTPS GET请求

实现复杂度

中:需实现本地代理、端口管理、双向转发

中:需集成OkHttp,处理请求重建和DNS解析

对业务代码侵入

低:WebView只需配置代理

低:仅需替换WebViewClient实现

Cookie/缓存/CORS

代理层透明,不额外处理

开发者需手动处理Cookie

维护成本

低:依赖官方API,版本升级风险小

低:基于稳定的WebViewClient API

失效降级策略

支持:代理故障可回退到系统网络

需自行实现

推荐场景

现代应用开发,需要完整协议支持

快速集成,仅需处理GET请求的场景

综合考虑稳定性、开发维护成本和未来趋势,强烈推荐使用本地代理方案。该方案能够处理完整的各种请求类型,具备完整的协议支持、透明的集成体验和优雅的降级机制,能以最低成本为用户提供稳定可靠的HTTPDNS服务。

建议开发者根据实际业务需求选择合适的接入策略,并进行充分的测试验证,确保在提升网络安全性的同时不影响用户体验。