背景

最近开发水印相机,遇到了用户网络正常,但是上传超时、上传失败的问题。通过听云后台看到接口错误记录中,用户的 localDNS 为空,于是就有了接入 HTTPDNS 的需求。

实践

由于项目中网络请求使用的 AFNetworking 框架,接入第三方 HTTPDNS 后,需要修改 AFNetworking 中的内容,才能让请求走IP。

大致流程是接入 SDK——>注册 SDK——>获取 IP——>存储——>使用。这里可依据个人情况,在启动时进行 SDK注册,获取 IP 有两种方式,一是只在 APP 启动时获取一次,然后存储起来,APP使用过程中不需要更新。二是在 每次某个接口使用时都获取。

下面详细来看看接入的过程

阿里 HTTPDNS

  1. 按照快速入门中的步骤进行配置
    • 添加域名,注意阿里的添加域名,可以添加全匹配和二级域名的方式
  2. 参考iOS SDK 接入进行接入
    1. 使用 CocoaPods 接入
      这里到要骂人的地方了,按照阿里自己的官方文档上面写的 CocoaPod 安装的SDK不是最新的
      1
      2
      3
      4
      5

      source 'https://github.com/CocoaPods/Specs.git'
      source 'https://github.com/aliyun/aliyun-specs.git'

      pod 'AlicloudHTTPDNS' # 注意,不能按照官方文档上的写法,后面不要加指定版本
    2. 项目中使用
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      - (void)registerAliDNS{
      HttpDnsService *httpdns = [[HttpDnsService alloc]initWithAccountID:@"Your Account ID" secretKey:@"Your Secret Key"];
      [httpdns setCachedIPEnabled:NO];
      // [httpdns cleanHostCache:nil];
      [httpdns setHTTPSRequestEnabled:YES];
      [httpdns setPreResolveHosts:@[
      @"baidu.com",
      ]];
      [httpdns setLogEnabled:YES];
      [httpdns setPreResolveAfterNetworkChanged:YES];
      // [httpdns setExpiredIPEnabled:YES];
      // [httpdns setDelegateForDegradationFilter:self];

      self.httpdns = httpdns;
      NSUInteger delaySeconds = 1;
      dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delaySeconds * NSEC_PER_SEC));
      dispatch_after(when, dispatch_get_main_queue(), ^{
      [self getUpFileHostIp];
      });
      }

      - (void)getUpFileHostIp {
      NSString *originalUrl = @"https://www.baidu.com";
      NSURL *url = [NSURL URLWithString:originalUrl];
      NSArray *ipsArray = [self.httpdns getIpsByHostAsync:url.host];
      if (!IsNilArray(ipsArray)) {
      self.hostIpStr = ipsArray.firstObject;
      }
      }

腾讯云 HTTPDNS

  1. 按照入门指南中的步骤进行配置
    1. 注册/登录账号
    2. 开通服务
    3. 在开发配置中申请应用
    4. 在域名管理中添加域名,注意添加腾讯设置域名,只能添加 xxx.com,不能添加 xxx.yyy.com 这种
  2. 参考iOS SDK 文档进行接入
    1. 使用 CocoaPods 接入
      1
      pod 'MSDKDns'
    2. 项目中使用
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      #import <MSDKDns/MSDKDns.h> // 腾讯 HTTP DNS

      - (void)registerMSDKDns {
      // 注意下面的使用方法,采用的是字典的方法来初始化,因为使用类的方法编译不通过。。。。
      [[MSDKDns sharedInstance] initConfigWithDictionary:@{
      @"dnsIp": @"119.29.29.98",
      @"dnsId": @"Your AppID",
      @"dnsKey": @"Your SecretKey", // 注意不同加密方式的 SecretKey 不同
      @"encryptType": @0, // 加密方式
      @"debug": @1,
      // @"routeIp": @"",
      }];

      NSString *hostStr = @"baidu.com";
      [[MSDKDns sharedInstance] WGGetHostByNameAsync:hostStr returnIps:^(NSArray *ipsArray) {
      NSLog(@"解析 IP:%@", ipsArray);
      if (ipsArray) {
      self.hostIpStr = ipsArray.firstObject;
      }
      }];
      }

使用

检测本地是否使用 HTTP、HTTPS 的代理,如果有代理,建议不要使用 HTTPDNS —— iOS SDK 文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

// 检测是否有 http 代理
- (BOOL)isUseHTTPProxy {
CFDictionaryRef dicRef = CFNetworkCopySystemProxySettings();
const CFStringRef proxyCFstr = (const CFStringRef)CFDictionaryGetValue(dicRef, (const void*)kCFNetworkProxiesHTTPProxy);
NSString *proxy = (__bridge NSString *)proxyCFstr;
if (proxy) {
return YES;
} else {
return NO;
}
}

// 检测是否有 https 代理
- (BOOL)isUseHTTPSProxy {
CFDictionaryRef dicRef = CFNetworkCopySystemProxySettings();
const CFStringRef proxyCFstr = (const CFStringRef)CFDictionaryGetValue(dicRef, (const void*)kCFNetworkProxiesHTTPSProxy);
NSString *proxy = (__bridge NSString *)proxyCFstr;
if (proxy) {
return YES;
} else {
return NO;
}
}

用 HTTPDNS 返回的 IP 替换掉 URL 中的域名,指定下 HTTP 头的 host 字段。

1
2
3
4
5
6
7
8
9
10

NSURL *httpDnsURL = [NSURL URLWithString:@"使用解析结果ip拼接的URL"];
float timeOut = 设置的超时时间;
NSMutableURLRequest *mutableReq = [NSMutableURLRequest requestWithURL:httpDnsURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval: timeOut];
[mutableReq setValue:@"原域名" forHTTPHeaderField:@"host"];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue currentQueue]];
NSURLSessionTask *task = [session dataTaskWithRequest:mutableReq];
[task resume];

项目中使用的是 AFNetworking,修改 AFNetworking 中 AFURLSessionMananger.m类:

  1. 添加evaluateServerTrust:forDmain:方法
  2. 修改URLSession:task:didReceiveChallenge:completionHandler:方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain {

//创建证书校验策略
NSMutableArray *policies = [NSMutableArray array];
if (domain) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}

//绑定校验策略到服务端的证书上
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

//评估当前 serverTrust 是否可信任,
//官方建议在 result = kSecTrustResultUnspecified 或 kSecTrustResultProceed 的情况下 serverTrust 可以被验证通过,
//https://developer.apple.com/library/ios/technotes/tn2232/_index.html
//关于SecTrustResultType的详细信息请参考SecTrust.h
SecTrustResultType result;
SecTrustEvaluate(serverTrust, &result);

return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}

- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
BOOL evaluateServerTrust = NO;
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *credential = nil;

if (self.authenticationChallengeHandler) {
id result = self.authenticationChallengeHandler(session, task, challenge, completionHandler);
if (result == nil) {
return;
} else if ([result isKindOfClass:NSError.class]) {
objc_setAssociatedObject(task, AuthenticationChallengeErrorKey, result, OBJC_ASSOCIATION_RETAIN);
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
} else if ([result isKindOfClass:NSURLCredential.class]) {
credential = result;
disposition = NSURLSessionAuthChallengeUseCredential;
} else if ([result isKindOfClass:NSNumber.class]) {
disposition = [result integerValue];
NSAssert(disposition == NSURLSessionAuthChallengePerformDefaultHandling || disposition == NSURLSessionAuthChallengeCancelAuthenticationChallenge || disposition == NSURLSessionAuthChallengeRejectProtectionSpace, @"");
evaluateServerTrust = disposition == NSURLSessionAuthChallengePerformDefaultHandling && [challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
} else {
@throw [NSException exceptionWithName:@"Invalid Return Value" reason:@"The return value from the authentication challenge handler must be nil, an NSError, an NSURLCredential or an NSNumber." userInfo:nil];
}
} else {
evaluateServerTrust = [challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}

if (evaluateServerTrust) {
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
objc_setAssociatedObject(task, AuthenticationChallengeErrorKey,
[self serverTrustErrorForServerTrust:challenge.protectionSpace.serverTrust url:task.currentRequest.URL],
OBJC_ASSOCIATION_RETAIN);
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
}

// 修改----------------------------部分
//获取原始域名信息
NSURLRequest *request = task.currentRequest;
NSString *host = [[request allHTTPHeaderFields] objectForKey:@"host"];
if (!host) {
host = challenge.protectionSpace.host;
}
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
}
}

if (completionHandler) {
completionHandler(disposition, credential);
}
}

测试

按照上面的接入步骤接入完成后进行测试,上传图片出现了:首次失败、再次成功的情况,失败的原因是,疑似不受信任服务器xxx.xx.xxx.xx,还需要再修改AFSecurityPolicy

1
2
3
4
5
6
7
8
9
10
11
12

+ (instancetype)defaultPolicy {
AFSecurityPolicy *securityPolicy = [[self alloc] init];
securityPolicy.SSLPinningMode = AFSSLPinningModeNone;

// 添加下面两行代码
securityPolicy.allowInvalidCertificates = YES;
securityPolicy.validatesDomainName = NO;

return securityPolicy;
}

加入后再次测试,完美运行

总结

  • 接入前,先考虑考虑要接入的方式,即:刷新 HTTPDNS 的逻辑
  • 接入时:
    • 阿里 HTTPDNS 需要注意 Pod 中依赖不要按照官方文档设置版本;
    • 腾讯的 HTTPDNS 则要注意初始化的方法;
    • 注意两家平台上设置域名时的不同;
  • 接入后:
    • 修改AFNetworking 中 AFURLSessionMananger.m
    • 修改AFSecurityPolicy

参考