美文网首页
iOS 使用NSURLProtocol拦截网络请求

iOS 使用NSURLProtocol拦截网络请求

作者: 宥落 | 来源:发表于2021-08-20 09:10 被阅读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;
    }
    

    相关文章

      网友评论

          本文标题:iOS 使用NSURLProtocol拦截网络请求

          本文链接:https://www.haomeiwen.com/subject/ninzbltx.html