iOS 使用NSURLProtocol拦截网络请求

2021-08-20  本文已影响0人  宥落

文章主要内容:拦截APP内的所有网络请求,并保存到本地,并解决AFNetworking无法拦截、拦截的post请求body为空等问题。不包含对WKWebview内的请求拦截,在拦截post请求时遇到点麻烦,如果有需要可以参考

接下来开始对AFNetworking的网络请求拦截,实现方式和DNS解析类似,都是通过自定义NSURLProtocol来实现。

1、注册自定义NSURLProtocol

按实际需要在合适的实际注册即可,自己这边的需求是拦截所有请求,故我在APPDelegate中注册,当然还需要注销:

自定义NSURLProtocol:

@interface YYURLProtocol : NSURLProtocol

注册/注销自定义的NSURLProtocol:

- (void)resisterYYURLProtocol
{
    // 注册我们自己的URLProtocol
    [NSURLProtocol registerClass:[self class]];
    
    [self exchangeAFNSessionConfiguration];
}

- (void)unregisterYYURLProtocol
{
    [NSURLProtocol unregisterClass:[self class]];
}

因为使用AFNetworking的网络请求,通过sessionWithConfiguration:delegate:delegateQueue:得到的session,他的configuration中已经有一个NSURLProtocol,因此它不会走我们的protocol。将NSURLSessionConfiguration的属性protocolClasses的get方法hook掉,通过返回我们自己的protocol,这样,我们就能够监控到通过sessionWithConfiguration:delegate:delegateQueue:得到的session的网络请求:

- (void)exchangeAFNSessionConfiguration{
    Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
    Method originalMethod = class_getInstanceMethod(cls, @selector(protocolClasses));
    Method stubMethod = class_getInstanceMethod([self class], @selector(protocolClasses));
    if (!originalMethod || !stubMethod) {
        [NSException raise:NSInternalInconsistencyException format:@"Couldn't load NEURLSessionConfiguration."];
    }
    method_exchangeImplementations(originalMethod, stubMethod);
}

- (NSArray *)protocolClasses{
    return @[[YYURLProtocol class]];
}

这样就完成了自定义NSURLProtocol的注册和注销

2、开始拦截

首先重写canInitWithRequest,通过返回值告诉NSURLProtocol对进来的请求是否拦截,这里可以按需求根据域名拦截想要拦截的网络请求:

// 开始之前定义一个key标记是否拦截过,防止无限循环
static NSString * const URLProtocolHandledKey = @"URLProtocolHandledKey";

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    // 看看是否已经处理过了
    if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
        return NO;
    }

    NSString *scheme = request.URL.scheme;
    // 只处理http和https请求
    if ( ([scheme caseInsensitiveCompare:@"http"] != NSOrderedSame && [scheme caseInsensitiveCompare:@"https"] != NSOrderedSame)) {
        return NO;
    }
        
    // 拦截所有
    return YES;
}

如不需要对request做特殊处理,按如下即可完成拦截:

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:request];
    return request;
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
    completionHandler(NSURLSessionResponseAllow);
    self.response = response;
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    [self.client URLProtocol:self didLoadData:data];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (!error) {
        //成功
        [self.client URLProtocolDidFinishLoading:self];
    } else {
        //失败
        [self.client URLProtocol:self didFailWithError:error];
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
    if (response != nil){
        self.response = response;
        [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
    }
}

3、将网络请求记录到本地

简单的需求:能查看接口对应的请求地址、请求参数、返回信息,你也可以保存更多的信息,比如header信息、请求方式等信息。

这里有一个问题就是:如何获取NSURLSessionTask中的body信息。虽然有提供task.originalRequest.HTTPBody,但是在实际获取的时候,task.originalRequest.HTTPBody的值为空。

解决方法:在canonicalRequestForRequest方法中,将request中的HTTPBodyStream信息,重新赋值到body中。具体如下:

新建NSURLRequest的分类:

@implementation NSURLRequest (YYLog)

- (NSURLRequest *)yy_getPostRequestIncludeBody{
    return [[self yy_getMutablePostRequestIncludeBody] copy];
}
 
- (NSMutableURLRequest *)yy_getMutablePostRequestIncludeBody{
    NSMutableURLRequest * req = [self mutableCopy];
    if ([self.HTTPMethod isEqualToString:@"POST"]) {
        if (!self.HTTPBody) {
            NSInteger maxLength = 1024;
            uint8_t d[maxLength];
            NSInputStream *stream = self.HTTPBodyStream;
            NSMutableData *data = [[NSMutableData alloc] init];
            [stream open];
            BOOL endOfStreamReached = NO;
            while (!endOfStreamReached) {
                NSInteger bytesRead = [stream read:d maxLength:maxLength];
                if (bytesRead == 0) {
                    endOfStreamReached = YES;
                } else if (bytesRead == -1) {
                    endOfStreamReached = YES;
                } else if (stream.streamError == nil) {
                    [data appendBytes:(void *)d length:bytesRead];
                }
            }
            req.HTTPBody = [data copy];
            [stream close];
        }
 
    }
    return req;
}

重新调整canonicalRequestForRequest中的代码:

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return [request yy_getPostRequestIncludeBody];
}

因为对request中的内容做了修改,所以需要重写startLoadingstopLoading方法:

- (void)startLoading {
    NSMutableURLRequest *request =  [self.request mutableCopy];

    // 标识该request已经处理过了,防止无限循环
    [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:request];
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    configuration.protocolClasses = @[[YYURLProtocol class]];

    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
    self.yytask = [session dataTaskWithRequest:request];
    [self.yytask resume];
}

- (void)stopLoading {
    [self.yytask cancel];
}

最后将我们需要的信息保存到本地,对于成功的网络请求,我选择直接保存返回数据,失败的请求保存error信息:

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    [self.client URLProtocol:self didLoadData:data];
    
    // 返回数据
    NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    [self recordLogToLocalWith:dataTask result:result];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    // 请求完成,成功或者失败的处理
    if (!error) {
        //成功
        [self.client URLProtocolDidFinishLoading:self];
    } else {
        //失败
        [self.client URLProtocol:self didFailWithError:error];
        [self recordLogToLocalWith:task result:error.description];
    }
}

- (void)recordLogToLocalWith:(NSURLSessionTask *)task result:(NSString *)result{
    // 请求url
    NSString *url = task.originalRequest.URL.absoluteString;
    
    // 请求方式
    NSString *method = task.originalRequest.HTTPMethod;

    // 请求body
    NSMutableURLRequest *mutableReqeust = [task.originalRequest mutableCopy];
    NSString *body = [[NSString alloc] initWithData:mutableReqeust.HTTPBody encoding:NSUTF8StringEncoding];
 
    // 保存相关代码
}

4、Other

关于HTTPS证书验证,好像也没用到,只是偷懒将之前做NDS解析的代码,copy过来改了一下:

- (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;
    }
    completionHandler(disposition,credential);
}

- (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);

    SecTrustResultType result;
    SecTrustEvaluate(serverTrust, &result);
    if (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed) {
        return YES;
    }
    return NO;
}
上一篇下一篇

猜你喜欢

热点阅读