iosiOS 移动端开发iOS_Skill_Collect

iOS-网络优化(一)-ip直连

2018-02-26  本文已影响380人  doudo

一、基础背景

1. DNS解析

现在假如我们访问一个网站www.baidu.com从按下回车到百度页面显示到我们的电脑上会经历如下几个步骤

其中第二步就是我们所说的DNS解析过程,域名和IP地址的关系其实就是我们的身份证号和姓名的关系,都是来标记一个人或者是一个网站的,只是IP地址\身份证号只是一串没有意义的数字,辨识度低,又不好记,所以就会在IP上加上一个域名以便区分,或是做的更加个性化,但是如果真的要来准确的区分还是要靠身份证号码或者是IP的,所以DNS解析就应运而生了。

2. 什么是DNS劫持

DNS劫持,是指在DNS解析过程中拦截域名解析的请求,然后做一些自己的处理,比如返回假的IP地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能反应或访问的是假网址。根本原因就是以下两点:

4. 防止DNS劫持

了解了DNS劫持的相关资料后我们就知道了,防止NDS劫持就要从第二步入手,因为DNS解析过程是运营商来操作的,我们不能去干涉他们,不然我们也就成了劫持者了,所以我们要做的就是在我们请求之前对我们的请求链接做一些修改,将我们原本的请求链接www.baidu.com 修改为180.149.132.47,然后请求出去,这样的话就运营商在拿到我们的请求后发现我们直接用的就是IP地址就会直接给我们放行,而不会去走他自己DNS解析了,也就是说我们把运营商要做的事情自己先做好了。不走他的DNS解析也就不会存在DNS被劫持的问题,从根本是解决了。

5. IP直连

它具有多方面的优势:

5. 获取IP

对于获取 IP,有两种方案:

  1. HTTPDNS

HTTPDNS是客户端基于http协议向服务器A发送域名B解析请求(例如:www.baidu.com),服务器A直接返回域名B对应的ip地址(例如:119.75.217.109),客户端获取到的IP后就向直接往此IP发送业务协议请求。
这种方式替代了基于DNS协议向运营商LocalDNS发起解析请求,可以从根本上避免LocalDNS造成的域名劫持问题。
常规的DNS解析是通过UDP方式。

国内提供域名解析 API 接口的,有 DNSPod,示例如下:
http://119.29.29.29/d?dn=www.163.com&ttl=1
// 输出如下:
183.47.248.109;125.90.206.144;14.215.100.95;183.6.245.191,17
现在国内有很多厂商为 DNSPod 开发了 SDK,比如 阿里、七牛(开源)等。不想自己写的,不妨使用这些 SDK。

  1. 内置IP列表
    可以在启动等阶段由服务端下发域名和 IP 的对应列表,客户端来进行缓存,发起网络请求的时候直接根据缓存 IP 来进行业务访问。

二、实际应用场景中的问题

实现 HTTP 协议下 IP 连接其实是很简单的,我们只需要通过 NSURLProtocol 来拦截网络请求,然后将符号条件的网络请求 URL 中的域名修改为 IP 就可以啦。

但是会有各种各样的问题:

1.http请求服务器无法判断请求访问的内容

原因:在我们修改http请求时,这时http的head中host字段会变成ip,因为一台服务器我们会有很多接口服务同时存在,服务器接收到请求后无法根据域名去判断我们访问的是哪个服务。

解决:由于服务器是根据host字段来判断请求的服务,所以在发起网络请求时,用带ip的URL生成request后,手动将request中的host字段改回域名。这样服务器可以正确识别,运营商也会根据域名中的ip为我们路由。

//原始URL
NSURL *originalUrl =[NSURL URLWithString:@"https://api.helijia.com/app-merchant"];
//根据原始URL获取 第三方解析出的ip
NSString *ip = [self getHostByUrlSyn:url];
//替换ip后的URL
NSURL *url = [ip replaceHostWithIp:ip];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//将request的host字段改为原始URL的域名
[request setValue:originalUrl.host forHTTPHeaderField:@"host"];
2. POST请求这块也算是一个大坑

我们知道http的post请求会包含一个body体,里面包含我们需要上传的参数等一些资料,对于POST请求我们的NSURLProtocol是可以正常拦截的,但是我们拦截之后发现无论怎么样我们获得的body体都为nil!后来查了一些资料发下又是苹果爸爸在做手脚。NSURLProtocol在拦截NSURLSession的POST请求时不能获取到Request中的HTTPBody,这个貌似早就国外的论坛上传开了,但国内好像还鲜有人知,据苹果官方的解释是Body是NSData类型,即可能为二进制内容,而且还没有大小限制,所以可能会很大,为了性能考虑,索性就拦截时就不拷贝了(内流满面脸)。为了解决这个问题,我们可以通过把Body数据放到Header中,不过Header的大小好像是有限制的,我试过2M是没有问题,不过超过10M就直接Request timeout了。。。而且当Body数据为二进制数据时这招也没辙了,因为Header里都是文本数据,另一种方案就是用一个NSDictionary或NSCache保存没有请求的Body数据,用URL为key,最后方法就是别用NSURLSession,老老实实用古老的NSURLConnection算了。。。你以为这么就结束了吗?并没有,后来查了大量的资料发现,既然post请求的httpbody没有苹果复制下来,那我们就不用httpbody,我们再往底层去看就会发现HTTPBodyStream这个东西我们可以通过他来获取请求的body体具体代吗如下

#pragma mark -
#pragma mark 处理POST请求相关POST  用HTTPBodyStream来处理BODY体
- (NSMutableURLRequest *)handlePostRequestBodyWithRequest:(NSMutableURLRequest *)request {
    NSMutableURLRequest * req = [request mutableCopy];
    if ([request.HTTPMethod isEqualToString:@"POST"]) {
        if (!request.HTTPBody) {
            uint8_t d[1024] = {0};
            NSInputStream *stream = request.HTTPBodyStream;
            NSMutableData *data = [[NSMutableData alloc] init];
            [stream open];
            while ([stream hasBytesAvailable]) {
                NSInteger len = [stream read:d maxLength:1024];
                if (len > 0 && stream.streamError == nil) {
                    [data appendBytes:(void *)d length:len];
                }
            }
            req.HTTPBody = [data copy];
            [stream close];
        }
    }
    return req;
}

这样之后的req就是携带了body体的request啦,可以愉快地做post请求啦。

3.Https请求证书校验错误

分析:
发送HTTPS请求首先要进行SSL/TLS握手,握手过程大致如下:

上述过程中,和HTTPDNS有关的是第3步,客户端需要验证服务端下发的证书,验证过程有以下两个要点:

当客户端使用HTTPDNS解析域名时,请求URL中的host会被替换成HTTPDNS解析出来的IP,所以在证书验证的第2步,会出现domain不匹配的情况,导致SSL/TLS握手不成功。
解决方案:只需在验证时,传入真实的 host 即可:

- (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);
}
/*
 * NSURLSession
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler
{
    if (!challenge) {
        return;
    }
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    NSURLCredential *credential = nil;
    /*
     * 获取原始域名信息。
     */
    NSString* host = [[self.request allHTTPHeaderFields] objectForKey:@"host"];
    if (!host) {
        host = self.request.URL.host;
    }
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
            disposition = NSURLSessionAuthChallengeUseCredential;
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        } else {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    } else {
        disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    }
    // 对于其他的challenges直接使用默认的验证方案
    completionHandler(disposition,credential);
}

4. webview中H5页面部分

HTTPDNS实施的主要难点与坑点都在H5页面上面,下面逐条记录下在实施webview的HTTPDNS时遇到的问题:由于web页面的请求并不是由客户端发起,我们无法在生成request的时候修改host。
解决:在这里我们使用NSURLProtocol来解决。

用一句话解释NSURLProtocol :NSURLProtocol就是一个苹果允许的中间人攻击。
NSURLProtocol可以劫持系统所有基于C socket的网络请求。
注意:WKWebView基于Webkit,并不走底层的C socket,所以NSURLProtocol拦截不了WKWebView中的请求。

具体步骤为:
注册NSURLProtocol子类 -> 使用NSURLProtocol子类拦截Webview请求 -> 使用NSURLSession重新发起请求 -> 将NSURLSession请求的响应内容返回给Webview

  1. NSURLProtocol子类的实现:
    拦截哪些请求
/**

 *  是否拦截处理指定的请求

 *

 *  @param request 指定的请求

 *

 *  @return 返回YES表示要拦截处理,返回NO表示不拦截处理

 */

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {

    //DNS开关控制功能开启关闭

    if (![[HLJHttpDNS shareInstance] isDNSConfigWorking]) {

        return NO;

    }

    /* 防止无限循环,因为一个请求在被拦截处理过程中,也会发起一个请求,这样又会走到这里,如果不进行处理,就会造成无限循环 */

    if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) {

        return NO;

    }

    // 防止无限循环, 第三方解析会发出ip域名的请求,这里筛选

    // 判断请求URL的Host是否Ipv4

    if ([WebViewURLProtocol checkHostIp:request.URL.host]) {

        return NO;

    }    

    NSString *url = [request.URL.host mutableCopy];

    //去掉Ipv6的大括号

    url = [url stringByReplacingOccurrencesOfString:@"[" withString:@""];

    url = [url stringByReplacingOccurrencesOfString:@"]" withString:@""];

    // 判断请求URL的Host是否Ipv6

    if ([WebViewURLProtocol checkHostIpv6:url]) {

        return NO;

    }

    NSMutableURLRequest *mutableReq = [request mutableCopy];

    //假设原始的请求头部没有host信息,只有使用IP替换后的请求才有

    NSString *host = [mutableReq valueForHTTPHeaderField:@"host"];

    if (!mutableReq && host) {

        return NO;

    }

    return YES;

}

在拦截的部分,我们需要注意一点,因为我们向第三方解析域名的请求也是ip的。这里我们需要在拦截时对域名的host位进行判断,如果是ipv4、ipv6的域名,就不对其进行拦截。不然程序就会循环拦截重新发起后的请求,导致程序卡死。
我们项目中图片服务是走CDN的服务器,还有其他统计等第三方的服务等等。我们将这类第三方的域名加入了白名单,在请求时会跳过对白名单内域名的拦截。

  1. 拦截住的请求怎么修改

拦截请求后,我们在重新发起的请求中对request进行修改:替换域名为解析后的ip、修改request的host

- (void)startLoading {

    NSMutableURLRequest *request = [self.request mutableCopy];

    // 表示该请求已经被处理,防止无限循环

    [NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];

    NSMutableURLRequest *mutableReq = [request mutableCopy];

    NSString *originalUrl = mutableReq.URL.absoluteString;

    NSURL *url = [NSURL URLWithString:originalUrl];

    // 同步接口获取IP地址

    NSString *ip = [[HLJHttpDNS shareInstance] getHostByNameSyn:url.absoluteString];    

    if (ip) {

        // 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置

        NSRange hostFirstRange = [originalUrl rangeOfString:url.host];

        if (NSNotFound != hostFirstRange.location) {

            mutableReq.URL = [NSURL URLWithString:ip];

            // 添加原始URL的host

            [mutableReq setValue:url.host forHTTPHeaderField:@"host"];

            // 添加originalUrl保存原始URL

            [mutableReq addValue:originalUrl forHTTPHeaderField:@"originalUrl"];

        }

    }

    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];

    self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue currentQueue]];

    NSURLSessionTask *task = [_session dataTaskWithRequest:mutableReq];

    [task resume];

}

在NSURLProtocol中拦截了请求后,在重新发起NSURLSession代理方法中,我们将证书校验的Host重新改回域名,这样就会通过证书校验过程。

#pragma NSURLSessionTaskDelegate

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler {

    if (!challenge) {

        return;

    }

    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;

    NSURLCredential *credential = nil;

    /*

     * 获取原始域名信息。

     */

    NSString *host = [[self.request allHTTPHeaderFields] objectForKey:@"host"];

    if (!host) {

        host = self.request.URL.host;

    }

    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {

        if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {

            disposition = NSURLSessionAuthChallengeUseCredential;

            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];

        } else {

            disposition = NSURLSessionAuthChallengePerformDefaultHandling;

        }

    } else {

        disposition = NSURLSessionAuthChallengePerformDefaultHandling;

    }

    // 对于其他的challenges直接使用默认的验证方案

    completionHandler(disposition, credential);

}

参考文献

上一篇 下一篇

猜你喜欢

热点阅读