iOS端WebView场景使用HTTPDNS

重要

本文中含有需要您注意的重要提示信息,忽略该信息可能对您的业务造成影响,请务必仔细阅读。

重要

本文档只针对WkWebView场景下如何使用HTTPDNS解析出的IP,关于HTTPDNS本身的解析服务,请先查看iOS SDK 开发手册

1. 前言

通过iOSNative场景使用HTTPDNS这篇文档,我们已经知道如何在iOS平台上的Native场景中如何使用HTTPDNS,实现防劫持、调度精准、解析及时生效的能力。

iOS上还有一个频繁发生网络请求的场景:WkWebView。WKWebViewWebKit框架提供的一个现代Web视图,用于在iOS应用中显示网页内容。它替代了旧的UIWebView,提供了更好的性能、更多的功能和更高的安全性。我们期望,在WkWebView加载网页的时候,也能使用HTTPDNS,提升网络安全性与网络性能。

2. 技术现状

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

  • iOS 17.0之前:Apple官方并未在WkWebView上开放DNS解析相关的hook接口,也并未直接开放自定义网络请求实现的接口,需要通过hook 私有API拦截流量的复杂方案实现。

  • iOS 17.0及以后:Apple引入了ProxyConfiguration相关API,为WKWebView提供了官方的代理配置能力,可以通过“接近完美”的方式拦截所有网络请求,使得优雅集成HTTPDNS成为可能。

根据Apple官方统计数据(截至202564日),在系统版本分布上,iOS 17+已经占全部iPhone设备的91%,且在持续增长中。因此,考虑到HTTPDNSWkWebView场景带来的是防劫持、调度精准、解析及时生效等非功能性提升,建议只需要在iOS 17+的系统版本上接入HTTPDNS,通过一个比较终态的接入方案,覆盖大部分客户,且旧版本系统用户,也会在后续的陆续版本升级中,逐渐享受这个能力。

3. 推荐方案:iOS 17+基于本地代理的方案

3.1 方案概述

iOS 17.0引入了ProxyConfiguration相关API,允许应用为WKWebView配置本地代理服务器。通过这种方式,我们可以在本地启动一个代理服务器,拦截WKWebView的所有网络请求,在代理层面实现HTTPDNS域名解析,然后将请求转发到真实服务器。

image

相比传统技术方案,本地代理方案具有以下显著优势:

  1. 稳定性:基于iOS 17+官方API,无需依赖私有API或混淆技术,稳定性和兼容性最佳。

  2. 适用性:对WebView完全透明,无需处理cookie、重定向、CORS等复杂细节。支持HTTP/HTTPS/WebSocket等所有协议,覆盖面更广。

  3. 易维护:实现逻辑清晰,维护成本相对较低。

3.2 接入参考

考虑创建本地代理服务、解析HTTP请求、基于HTTPDNS解析结果创建连接、数据转发等步骤有一定实现成本,我们在Demo中提供了一个实现作为参考。您可以结合业务实际情况,按需调整这份代码,使得更符合您的业务需要。

直接引入Demo中的`HttpdnsLocalHttpProxy.h`和`HttpdnsLocalHttpProxy.m`文件后,在WkWebView初始化时,只需要进行如下配置,结合HTTPDNS,就可以实现WebView中的网络请求,都使用HTTPDNS进行域名解析了。

// 创建 WKWebViewConfiguration
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];

// 配置WebView代理
BOOL proxyConfigured = [HttpdnsLocalHttpProxy installIntoWebViewConfiguration:config];
if (proxyConfigured) {
    NSLog(@"WKWebViewProxyScenario: Proxy configuration applied successfully");
} else {
    NSLog(@"WKWebViewProxyScenario: Using system network (proxy unavailable)");
}

// 配置DNS解析器
[HttpdnsLocalHttpProxy setDNSResolverBlock:^NSString *(NSString *hostname) {
    // 获取HTTPDNS服务实例
    HttpDnsService *httpdns = [HttpDnsService sharedInstance];
    if (!httpdns) {
        return hostname;
    }

    // 使用HTTPDNS解析域名
    HttpdnsResult *result = [httpdns resolveHostSync:hostname byIpType:HttpdnsQueryIPTypeAuto];

    if (result) {
        NSString *resolvedIP = nil;

        // 使用IPv4地址
        if ([result hasIpv4Address]) {
            resolvedIP = [result firstIpv4Address];
            NSLog(@"WKWebViewProxyScenario: HTTPDNS IPv4 resolved: %@ -> %@", hostname, resolvedIP);
        } else if ([result hasIpv6Address]) {
            // 如果没有IPv4地址,使用IPv6地址
            resolvedIP = [result firstIpv6Address];
            NSLog(@"WKWebViewProxyScenario: HTTPDNS IPv6 resolved: %@ -> %@", hostname, resolvedIP);
        }

        if (resolvedIP) {
            return resolvedIP;
        }
    }

    NSLog(@"WKWebViewProxyScenario: didn't get an IP from HTTPDNS, fallback to use original hostname: %@", hostname);
    return hostname;
}];

[HttpdnsLocalHttpProxy setLogLevel:HttpdnsProxyLogLevelDebug];

WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
重要

在上线前,您需要阅读和理解代码实现逻辑,并进行充分测试,确保完美兼容。

3.3 实现细节

本章节介绍本地代理方案的核心实现流程,仅摘录核心代码片段。如果需要完整理解技术细节,请参考我们开源的代码实现。

  1. 创建本地代理服务:使用Network framework在本地启动HTTP代理服务器。

    说明

    如果在当前应用或者多个应用中存在多个本地代理服务器,待创建的新的本地代理服务器的端口可能会存在冲突,可以使用随机端口和重试机制降低冲突的可能。

    // 配置网络连接参数
    nw_parameters_t parameters = nw_parameters_create_secure_tcp(
        NW_PARAMETERS_DISABLE_PROTOCOL,    // 禁用TLS,代理本身不加密
        NW_PARAMETERS_DEFAULT_CONFIGURATION
    );
    
    // 启用地址重用,便于快速重启和端口复用
    nw_parameters_set_reuse_local_address(parameters, true);
    
    // 创建本地回环地址端点,仅监听本地连接
    NSString *portString = [NSString stringWithFormat:@"%d", port];
    nw_endpoint_t localEndpoint = nw_endpoint_create_host("127.0.0.1", [portString UTF8String]);
    nw_parameters_set_local_endpoint(parameters, localEndpoint);
    
    // 使用配置创建网络监听器
    _listener = nw_listener_create(parameters);
  2. 解析从WkWebView下发的connect代理建连请求。无论是何种请求,都会通过Connect方法建立隧道,然后使用这条连接承载HTTP协议报文。

    // 接收客户端请求数据
    nw_connection_receive(connection, 1, 4096, ^(dispatch_data_t content, ...) {
        NSString *requestLine = /* 解析请求数据 */;
        
        if ([requestLine hasPrefix:@"CONNECT "]) {
            // HTTPS 隧道请求:CONNECT example.com:443 HTTP/1.1
            [self handleHTTPSConnect:request connection:connection];
        }
    });
  3. 实现双向数据转发:在客户端和目标服务器之间建立透明的数据通道。

    - (void)relayDataFrom:(nw_connection_t)source to:(nw_connection_t)destination {
        nw_connection_receive(source, 1, 8192, ^(dispatch_data_t content, ...) {
            if (content && dispatch_data_get_size(content) > 0) {
                // 转发数据到目标连接
                nw_connection_send(destination, content, NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, 
                                 false, ^(nw_error_t error) {
                    if (!error) {
                        // 递归继续接收数据
                        [self relayDataFrom:source to:destination];
                    }
                });
            }
        });
    }
  4. 配置WKWebView代理:使用iOS 17.0引入的ProxyConfiguration完成WKWebView代理配置。

    if (@available(iOS 17.0, *)) {
        // 创建代理端点配置
        NSString *proxyHost = @"127.0.0.1";
        NSString *proxyPortString = [NSString stringWithFormat:@"%d", _proxyPort];
        nw_endpoint_t proxyEndpoint = nw_endpoint_create_host([proxyHost UTF8String], [proxyPortString UTF8String]);
    
        // 创建HTTP CONNECT代理配置
        nw_proxy_config_t proxyConfig = nw_proxy_config_create_http_connect(proxyEndpoint, NULL);
        if (proxyConfig) {
            WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore];
            NSArray<nw_proxy_config_t> *proxyConfigs = @[proxyConfig];
    
            // 检查API可用性并设置代理配置
            if ([dataStore respondsToSelector:@selector(setProxyConfigurations:)]) {
                [dataStore setProxyConfigurations:proxyConfigs];
                configuration.websiteDataStore = dataStore;
            }
        }
    }
  5. 集成HTTPDNS:代理直接使用HTTPDNS解析结果向目标服务器建连。

    // 使用HTTPDNS解析域名并创建到目标服务器的连接
    NSString *resolvedHost = [self resolveHostname:host];
    
    NSString *portString = [NSString stringWithFormat:@"%d", port];
    nw_endpoint_t remoteEndpoint = nw_endpoint_create_host([resolvedHost UTF8String], [portString UTF8String]);
    
    // 创建连接,禁用TLSTCP连接参数(代理本身不加密)
    nw_parameters_t params = nw_parameters_create_secure_tcp(
        NW_PARAMETERS_DISABLE_PROTOCOL,
        NW_PARAMETERS_DEFAULT_CONFIGURATION
    );
    
    // 启用地址重用,便于端口快速重绑定
    nw_parameters_set_reuse_local_address(params, true);
        
    nw_connection_t connection = nw_connection_create(remoteEndpoint, params);
    
    // 设置连接队列
    nw_connection_set_queue(connection, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0));

3.4 降级及优化方案

HttpdnsLocalHttpProxy 在设计之初即充分考虑了多种异常场景,内建多层防护与自动降级(Fallback)机制,旨在提供高性能本地代理能力的同时,保障系统的高可用性与最终网络请求的可达性,即便在组件故障或外部依赖异常的情况下,也能最大程度减少对用户体验的影响。

以下是不同的异常场景及应对:

场景

触发条件

降级或保护措施

最终效果

代理启动

端口冲突

自动重试随机端口

提高启动成功率

启动阻塞

启动超时退出

避免应用卡死

代理运行

nw_listener 崩溃

标记服务为“未运行”

触发后续降级策略

外部访问尝试

仅监听 127.0.0.1

拒绝局域网访问,保障安全

WebView 配置

服务未运行

使用非持久化 WKWebsiteDataStore

回退到系统默认网络

系统版本低于iOS17

跳过代理配置

与系统默认行为保持一致

DNS 解析

自定义解析器抛异常

捕获异常,使用原始域名,走localDNS解析

避免因解析失败影响连接

网络连接

目标服务器连接失败

返回 502 Bad Gateway 响应

提供标准化故障反馈

通过以上多层设计,HttpdnsLocalHttpProxy 能够在复杂多变的网络环境中实现可靠运行。其完备的降级机制确保即使部分组件失效,仍能保障请求通路不中断。

4. 自定义WKURLSchemeHandler的方案

EMAS目前提供了一个实验性质的参考方案:EMASCurlWeb,该方案在Github上开源,可以考虑参考这个方案,自行结合业务需要,实现一个托管WkWebView网络请求的完整方案。在此基础上,还可以根据业务需要,进一步实现资源预取、自定义缓存等能力。

重要

此方案从使用界面来说提供了比较好的体验,无需额外考虑缓存、cookie、重定向等细节,但它内部实现复杂,需要接入时在一定程度上理解和改造这个方案来适配业务需要。

5. 全局拦截NSURLProtocol的方案

基于NSURLProtocol可拦截iOS系统上基于上层网络库NSURLConnection/NSURLSession发出的网络请求,WkWebView发出的请求同样包含在内。步骤如下:

  1. 通过以下接口注册自定义NSURLProtocol,用于拦截WkWebView上层网络请求,并创建新的网络请求接管数据发送、接收、重定向等处理逻辑,将结果反馈给原始请求。

    [NSURLProtocol registerClass:[HttpDnsNSURLProtocolImpl class]];
  2. 自定义NSURLProtocol处理过程概述:

    • canInitWithRequest中过滤需要做HTTPDNS域名解析的请求。

    • 请求拦截后,做HTTPDNS域名解析。

    • 解析完成后,同普通请求一样,替换URL.host字段,替换HTTP Header Host域,并接管该请求的数据发送、接收、重定向等处理。

    • 通过NSURLProtocol的接口,将请求处理结果反馈到WebView原始请求。

  3. 自定义NSURLProtocol的实现逻辑可参考我们提供的demo中的 HttpDnsNSURLProtocolImpl.m

警告

由于Apple官方并未公开太多协议细节,上述方案在生产环境需要结合业务情况,自行处理cookie、重定向等细节问题,才具备可行性。

6. 方案总结与对比

本文档介绍了在iOSWKWebView场景下集成HTTPDNS的三种主要技术方案:基于iOS 17+的本地代理方案、自定义WKURLSchemeHandler方案以及全局拦截NSURLProtocol的方案。

每种方案都有其特定的适用场景、优缺点和实现复杂度。为帮助您快速决策,下表对这三种方案进行了横向对比:

维度 / 方案

本地代理(ProxyConfiguration)(iOS 17 +)

自定义 WKURLSchemeHandler(EMASCurlWeb 示例)

全局 NSURLProtocol 拦截

官方支持度

Apple 在 iOS 17 引入的正式 API,完全公开、长期可用

基于私有 WKURLSchemeHandler API,但需要自行实现完整网络栈

使用公开 NSURLProtocol API,但在 WKWebView 内部细节缺乏官方文档

适用系统版本

iOS 17 及以上

iOS 11 及以上(WKWebView 可用)

iOS 11 及以上

协议覆盖

HTTP / HTTPS / WebSocket / HTTP2(透明转发原始流量)

受限于自定义实现:HTTP / HTTPS(需自行扩展其他协议)

仅限 NSURLSession / NSURLConnection 可处理的协议

实现复杂度

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

高:需重写请求流水线、缓存、Cookie、重定向等

中:需处理请求复写、线程与缓存交互

对业务代码侵入

低:WKWebView 只需配置代理

低:替换 WKWebView 的 scheme handler

中:全局注册 NSURLProtocol,可能影响现有 NSURLSession 逻辑

Cookie / 缓存 / CORS

代理层透明,不额外处理

由方案内部模拟,需自测完整性

开发者需自行维护,容易遗漏边缘场景

维护成本

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

高:社区示例,需长期跟进 WKWebView 变更

中:系统 API 稳定,但需关注WKWebView 内部行为变化

失效降级策略

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

需自行实现

需自行实现

推荐场景

主推方案:目标用户 > iOS 17;对安全与兼容要求高

对旧系统版本仍需 HTTPDNS,且愿意投入较多研发资源

轻量改造、快速验证;对复杂请求兼容性要求不高

综合考虑稳定性、开发维护成本和未来趋势,我们强烈推荐使用基于iOS 17+的本地代理方案

随着iOS 17+系统版本的高速普及,该方案能以最低的成本为绝大多数用户带来稳定、可靠的HTTPDNS服务,有效解决域名劫持问题,并提升网络性能。其优雅的实现方式和官方支持,确保了方案的长期可行性。对于低于iOS 17的系统版本,采取平滑降级策略,即直接使用系统默认的网络请求,待用户升级系统后自动享受HTTPDNS带来的优势。

其他两种方案由于其固有的复杂性和不确定性,仅建议在有特殊需求且具备深厚技术储备的团队,经过充分评估和测试后谨慎采用。