iOS 使用NSURLProtocol拦截网络请求
文章主要内容:拦截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
中的内容做了修改,所以需要重写startLoading
和stopLoading
方法:
- (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;
}