通过URLProtocol拦截网络请求后进行ip替换或请求代理设置

一、关于 NSURLProtocol

NSURLProtocol作为URL Loading System中的一个独立部分存在,能够拦截所有的URL Loading System发出的网络请求,拦截之后便可根据需要做各种自定义处理,是iOS网络层的一个终极利器。虽然名叫NSURLProtocol,但它却不是协议。它是一个抽象类。我们要使用它的时候需要创建它的一个子类。
NSURLProtocol在iOS系统中大概处于这样一个位置:


Snip20181229_3.png

二、NSURLProtocol能拦截哪些网络请求

NSURLProtocol能拦截所有基于URL Loading System的网络请求。
URL Loading System的图如下:


872807-b7f17b6fbaf25831.png

所以,可以拦截的网络请求包括NSURLSession,NSURLConnection。现在主流的iOS网络库,例如AFNetworking,Alamofire等网络库都是基于NSURLSession或NSURLConnection的,所以这些网络库的网络请求都可以被NSURLProtocol所拦截。

PS:基于CFNetwork的网络请求,以及WKWebView的请求是无法拦截的。例如ASIHTTPRequest,MKNetwokit等网路库都是基于CFNetwork的,所以这些网络库的网络请求无法被NSURLProtocol拦截。

三、使用

使用NSURLProtocol的主要可以分为5个步骤:


Snip20181229_4.png

1、创建NSURLProtocol子类

由于NSURLProtocol是一个抽象类,要使用它的时候需要创建它的一个子类。.m文件如下:

#import "HJQURLProtocol.h"

static NSString * const URLProtocolHandledKey = @"URLProtocolHandledKey";

@interface HJQURLProtocol()<NSURLSessionDelegate>
@property(nonatomic,strong)NSURLSession * session;
@end

@implementation HJQURLProtocol

+(BOOL)canInitWithRequest:(NSURLRequest *)request
{
  //是http或https则拦截处理
  NSString * scheme = [[request.URL scheme] lowercaseString];//获取URL转小写
  if ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"])
  {
    //有拦截处理过的则不再拦截,否则会在这死循环
    if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request])
    {
      return NO;
    }
    else
    {
      return YES;
    }
  }
  else
  {
    return NO;
  }
}

//改变请求request
+(NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
  return request;
}

//开始请求
-(void)startLoading
{
  NSMutableURLRequest * mutableRequest = [[self request] mutableCopy];
  //标识该request已经处理过了,防止无限循环
  [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableRequest];
  
  
  //设置代理
  NSString * proxyHost = @"127.0.0.1";
  NSNumber * proxyPort = [NSNumber numberWithInt:8888];
  
  //创建一个代理服务器,包括HTTP或HTTPS代理,当然还可以添加SOCKS,FTP,RTSP等
  NSDictionary * proxyDic = @{
                              @"HTTPEnable": @YES,
                              @"HTTPProxy": proxyHost,
                              @"HTTPPort": proxyPort,
                              @"HTTPSEnable": @YES,
                              @"HTTPSProxy": proxyHost,
                              @"HTTPSPort": proxyPort
                              };
  
  NSURLSessionConfiguration * configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];//创建一个临时会话配置
  configuration.connectionProxyDictionary = proxyDic;
  
  //网络请求
  self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
  NSURLSessionTask * task = [self.session dataTaskWithRequest:self.request];
  [task  resume];//开始任务
  
}

//停止请求
-(void)stopLoading
{
  [self.session invalidateAndCancel];
  self.session = nil;
}

#pragma mark ---- NSURLSessionDelegate
/*
  NSURLSessionDelegate接到数据后,通过URLProtocol传出去
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
  if (error)
  {
    [self.client URLProtocol:self didFailWithError:error];
  }
  else
  {
    [self.client URLProtocolDidFinishLoading:self];
  }
}

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

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

@end

2、注册protocol

基于NSURLConnection[NSURLSession sharedSession]创建的网络请求,在AppDelegate的didFinishLaunchingWithOptions方法中调用registerClass方法即可。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  // Override point for customization after application launch.
  
  //注册protocol
  [NSURLProtocol registerClass:[HJQURLProtocol class]];
  
  return YES;
}

3、拦截用户的网络请求

首先,在拦截到网络请求后会先调用+(BOOL)canInitWithRequest:(NSURLRequest *)request方法。我们可以在该方法里进行是否处理这个拦截的逻辑。如设置只对拦截到的http或https请求进行处理。

+(BOOL)canInitWithRequest:(NSURLRequest *)request
{
  //是http或https则拦截处理
  NSString * scheme = [[request.URL scheme] lowercaseString];//获取URL转小写
  if ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"])
  {
    //有拦截处理过的则不再拦截,否则会在这死循环
    if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request])
    {
      return NO;
    }
    else
    {
      return YES;
    }
  }
  else
  {
    return NO;
  }
}

接着,会调用+(NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request在该方法中,我们可以对request进行处理。例如修改头部信息等。最后返回一个处理后的request实例。也可以在该方法里面将用户的请求域名替换成别的域名:

+(NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
  if ([request.URL host].length == 0)
  {
    return request;
  }

  NSString * originUrlStr = [request.URL absoluteString];
  NSString * originHostStr = [request.URL host];
  NSRange hostRange = [originUrlStr rangeOfString:originHostStr];
  
  if (hostRange.location == NSNotFound)
  {
    return request;
  }

  
  //定向到百度搜索
  NSString * ip = @"www.baidu.com";
  NSString * urlStr = [originUrlStr stringByReplacingCharactersInRange:hostRange withString:ip];
  NSURL * url = [NSURL URLWithString:urlStr];
  request.URL = url;

  return request;
}

4、转发

-(void)startLoading将处理过的request重新发送出去。发送的形式,可以是基于NSURLConnection,NSURLSession甚至CFNetwork。我们也可以在该方法里面设置网络代理,如下我们设置一个代理后,重新创建一个NSURLSession将网络请求发送出去:

//开始请求
-(void)startLoading
{
  NSMutableURLRequest * mutableRequest = [[self request] mutableCopy];
  //标识该request已经处理过了,防止无限循环
  [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableRequest];
  
  //设置代理
  NSString * proxyHost = @"127.0.0.1";
  NSNumber * proxyPort = [NSNumber numberWithInt:8888];
  
  //创建一个代理服务器,包括HTTP或HTTPS代理,当然还可以添加SOCKS,FTP,RTSP等
  NSDictionary * proxyDic = @{
                              @"HTTPEnable": @YES,
                              @"HTTPProxy": proxyHost,
                              @"HTTPPort": proxyPort,
                              @"HTTPSEnable": @YES,
                              @"HTTPSProxy": proxyHost,
                              @"HTTPSPort": proxyPort
                              };
  
  NSURLSessionConfiguration * configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];//创建一个临时会话配置
  configuration.connectionProxyDictionary = proxyDic;
  
  //网络请求
  self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
  NSURLSessionTask * task = [self.session dataTaskWithRequest:self.request];
  [task  resume];//开始任务
}

5、回调

上面使用的是NSURLSession请求,所以我们通过NSURLSessionDelegate来接收网络请求的数据(成功或失败等信息):

#pragma mark ---- NSURLSessionDelegate
/*
  NSURLSessionDelegate接到数据后,通过URLProtocol传出去
 */
//失败
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
  if (error)
  {
    [self.client URLProtocol:self didFailWithError:error]; //请求错误
  }
  else
  {
    [self.client URLProtocolDidFinishLoading:self]; //完成加载
  }
}
//接收到响应
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
  
  [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; //创建一个响应(缓存策略:不缓存)
  completionHandler(NSURLSessionResponseAllow);
  
}
//接收到数据
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
  [self.client URLProtocol:self didLoadData:data]; //接收到数据
}

然后再通过URLProtocol回调给外面用户发送网络请求的地方。不管外面用户是通过NSURLSession或NSURLConnection发起的网络请求,都可以收到URLProtocol的回调数据。URLProtocol回调的方法主要有以下几个:

[self.client URLProtocol:self didFailWithError:error]; //请求错误
[self.client URLProtocolDidFinishLoading:self]; //完成加载
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; //创建一个响应(缓存策略:不缓存)
[self.client URLProtocol:self didLoadData:data]; //接收到数据

6、结束

-(void)stopLoading完成网络请求的操作

//结束请求
-(void)stopLoading
{
  [self.session invalidateAndCancel];
  self.session = nil;
}

四、代码

AppDelegate.m文件

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  // Override point for customization after application launch.
  
  //注册protocol
  [NSURLProtocol registerClass:[HJQURLProtocol class]];
  
  return YES;
}

ViewController.m文件

#import "ViewController.h"

@interface ViewController ()<NSURLSessionDelegate>
@property(nonatomic,strong)NSMutableData *responseData;
@end

@implementation ViewController

- (void)viewDidLoad {
  [super viewDidLoad];
 
  UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
  btn.frame = CGRectMake(0, 300, [UIScreen mainScreen].bounds.size.width, 50);
  [btn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
  [btn setTitle:@"网络请求" forState:UIControlStateNormal];
  [btn addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
  [self.view addSubview:btn];
}

#pragma mark ---- 按钮响应函数
-(void)buttonAction:(UIButton *)button
{
   [button setTitle:@"请求中..." forState:UIControlStateNormal];
  
  
//     //NSURLSession的网络请求
//    NSURLSession * session = [NSURLSession sharedSession];
//    NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com"]];
//    NSURLSessionTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//        NSLog(@"NSURLSession got the response [%@]", response);
//        NSLog(@"NSURLSession got the data [%@]", data);
//    }];
//    [task resume];
  
  
   //NSURLConnection的网络请求
  NSURL * url = [NSURL URLWithString:@"https://www.baidu.com"];
  NSURLRequest * request = [NSURLRequest requestWithURL:url];
  NSURLConnection * connection = [NSURLConnection connectionWithRequest:request delegate:self];
  [connection start];
}

#pragma mark ---- NSURLConnectionDataDelegate代理方法
/*
 *当接收到服务器的响应(连通了服务器)时会调用
 */
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
  NSLog(@"接收到服务器的响应");
  //初始化数据
  self.responseData = [NSMutableData data];
}

/*
 *当接收到服务器的数据时会调用(可能会被调用多次,每次只传递部分数据)
 */
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
  NSLog(@"接收到服务器的数据");
  //拼接数据
  [self.responseData appendData:data];
}

/*
 *当服务器的数据加载完毕时就会调用
 */
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
  NSLog(@"服务器的数据加载完毕");
  //     NSLog(@"responseData:%@",self.responseData);
  
  NSString * jsonStr = [[NSString alloc] initWithData:self.responseData encoding:NSUTF8StringEncoding];
  
  NSLog(@"jsonStr:%@",jsonStr);
}

/*
 *请求错误(失败)的时候调用(请求超时\断网\没有网\,一般指客户端错误)
 */
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
  NSLog(@"请求错误:%@",error);
}
@end

HJQURLProtocol.m文件

#import "HJQURLProtocol.h"

static NSString * const URLProtocolHandledKey = @"URLProtocolHandledKey";

@interface HJQURLProtocol()<NSURLSessionDelegate>
@property(nonatomic,strong)NSURLSession * session;
@end

@implementation HJQURLProtocol

+(BOOL)canInitWithRequest:(NSURLRequest *)request
{
  //是http或https则拦截处理
  NSString * scheme = [[request.URL scheme] lowercaseString];//获取URL转小写
  if ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"])
  {
    //有拦截处理过的则不再拦截,否则会在这死循环
    if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request])
    {
      return NO;
    }
    else
    {
      return YES;
    }
  }
  else
  {
    return NO;
  }
}

//改变请求request
+(NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
  return request;
}

//开始请求
-(void)startLoading
{
  NSMutableURLRequest * mutableRequest = [[self request] mutableCopy];
  //标识该request已经处理过了,防止无限循环
  [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableRequest];
  
  
  //设置代理
  NSString * proxyHost = @"127.0.0.1";
  NSNumber * proxyPort = [NSNumber numberWithInt:8888];
  
  //创建一个代理服务器,包括HTTP或HTTPS代理,当然还可以添加SOCKS,FTP,RTSP等
  NSDictionary * proxyDic = @{
                              @"HTTPEnable": @YES,
                              @"HTTPProxy": proxyHost,
                              @"HTTPPort": proxyPort,
                              @"HTTPSEnable": @YES,
                              @"HTTPSProxy": proxyHost,
                              @"HTTPSPort": proxyPort
                              };
  
  NSURLSessionConfiguration * configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];//创建一个临时会话配置
  configuration.connectionProxyDictionary = proxyDic;
  
  //网络请求
  self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
  NSURLSessionTask * task = [self.session dataTaskWithRequest:self.request];
  [task  resume];//开始任务
  
}

//结束请求
-(void)stopLoading
{
  [self.session invalidateAndCancel];
  self.session = nil;
}

#pragma mark ---- NSURLSessionDelegate
/*
  NSURLSessionDelegate接到数据后,通过URLProtocol传出去
 */
//失败
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
  if (error)
  {
    [self.client URLProtocol:self didFailWithError:error]; //请求错误
  }
  else
  {
    [self.client URLProtocolDidFinishLoading:self]; //完成加载
  }
}
//接收到响应
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
  
  [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; //创建一个响应(缓存策略:不缓存)
  completionHandler(NSURLSessionResponseAllow);
  
}
//接收到数据
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
  [self.client URLProtocol:self didLoadData:data]; //接收到数据
}

@end

效果:你以为你请求的是https://www.baidu.com,但其实已经转到127.0.0.1代理服务器上了

Snip20181228_16.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,036评论 1 32
  • 【文学群·凌逍逸】/文 昨天发布了六月的总结,早上在和小伙伴评论互动时有了以下对话。 早上吃饭的时候,我一直在品味...
    凌逍逸阅读 536评论 2 2
  • 你和谁一样,谁和你一样 有人生,有人死 这是世间的规律,谁也无法逃脱 你来到人间一趟,为什么来到这里 你的使命是什...
    诗人東邪阅读 138评论 0 0
  • 与坚持无关 墨烨日更百日 之第77天 忙碌的加班缝隙,看《光荣与梦想》。1932年3月4日,12点,富兰克林·罗...
    静若无心阅读 1,173评论 0 0
  • 只是我所了解的天蝎座 天蝎座 你确定被天蝎座爱过吗?天蝎座的爱,只有一个词,那就是“迷恋”——纯粹而深度的迷恋,无...
    沧山映水阅读 411评论 0 1