iOS拦截重定向请求(302)的几种方式

在多数情况下,我们看到的网络请求都是200的状态码,比如使用基于Oauth2认证协议的API,在认证阶段,需要提供一个回调地址,当用户授权后,服务器会返回一个302Response,Response Header中会有一个Localtion的字段,包含了我们的回调地址,同时会有一个Code的参数

UIWebView控件

这是最常见的做法,但是UIWebview是无法拦截302请求的,只能等到整个流程完成回到调用地址时,我们在webview控件的Webview的webViewDidFinishLoad回调方法处理数据。

基于NSURLProtocol的实现

使用场景:
  • 重定向网络请求
  • 忽略网络请求,使用本地缓存
  • 自定义网络请求的返回结果
  • 一些全局的网络请求设置
拦截网络请求
定义于注册
1
2
3
4
5
6
7
8
9
@interface CustomURLProtocol : NSURLProtocol
@end

注册:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//注册protocol
[NSURLProtocol registerClass:[CustomURLProtocol class]];
return YES;
}
使用NSURLConnection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface CustomerURLProtocal()<NSURLConnectionDelegate>

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
NSString *scheme = [[request URL] scheme];
if ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) {
//看看是否已经处理过了,防止无限循环
if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
return NO;
}
return YES;
}
return NO;
}
@end
常用接口说明(由于NSURLProtocol是抽象类,下列的方法需要被实现)
关于 +canInitWithRequest:

这个方法主要是说明你是否打算处理对应的request,如果不打算处理,返回NO,URL loading System会使用系统默认的行为去处理,如果打算处理,返回YES,然后你需要处理改请求的所有东西,包括获取请求数据并返回给URL Loading System。 网络数据可以简单通过NSURLConnection去获取,而且每个NSURLProtocol对象都有一个NSURLProtocolClient实例,可以用过改client将获取到的数据返回URL Loading System

需要注意的地方

当你加载一个URL资源,URL Loading System会询问CustomerURLProtocol是否能处理该请求,你返回YES,然后URL Loading System会创建一个 Customer Protocol 实例,然后调用 NSURLConnection 去获取数据,然而这也会调用 URL Loading System , 而你在 +canInitWithRequest: 中又总会返回YES,这样URL Loading System又会创建CustomerURLProtocol实例导致无线的循环,我们应该保证每个request只被处理一次,可以通过+setProperty:forKey:inRequest:标示那些已经处理的request,然后+canInitWithRequest()中查询request是否已经处理过了,如果是则返回NO.

+canonicalRequestForRequest

通常改方法可以简单的直接返回request,但也可以在这里修改request,比如添加header,修改host等,返回一个新的request,这是一个抽象方法,子类必须实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
mutableReqeust = [self redirectHostInRequset:mutableReqeust];
return mutableReqeust;
}

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

NSString *originUrlString = [request.URL absoluteString];
NSString *originHostString = [request.URL host];
NSRange hostRange = [originUrlString rangeOfString:originHostString];
if (hostRange.location == NSNotFound) {
return request;
}
//定向到bing搜索主页
NSString *ip = @"cn.bing.com";

// 替换域名
NSString *urlString = [originUrlString stringByReplacingCharactersInRange:hostRange withString:ip];
NSURL *url = [NSURL URLWithString:urlString];
request.URL = url;

return request;
}
+requestIsCacheEquivalent:toRequest:

主要判断两个request是否相同,如果相同的话可以使用缓存数据,而且需要调用父类的实现

1
2
3
4
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
return [super requestIsCacheEquivalent:a toRequest:b];
}

-startLoding -stopLoading

这两个方法主要开始和取消相应的request,而且需要表示那些已经处理好的request。

1
2
3
4
5
6
7
8
9
- (void)startLoading {
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
//标示改request已经处理过了,防止无限循环
[NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust];
self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self];
}
- (void)stopLoading {
[self.connection cancel];
}

NSURLConnectionDataDelegate方法:
在处理网络请求的时候会调用该代理方法,我们需要将收到的消息通过client进行返回给URL Loading System。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}

- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.client URLProtocol:self didLoadData:data];
}

- (void) connectionDidFinishLoading:(NSURLConnection *)connection {
[self.client URLProtocolDidFinishLoading:self];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[self.client URLProtocol:self didFailWithError:error];
}

使用NSURLSession

比较

跟NSURLConnection很像,他们只是在封装请求哪里有所区别,并且由于iOS8提供了canInitWithTask:这个方法。所以在使用Session进行封装的时候记得处理这个方法。虽然这个方法只有iOS8提供了,但是在iOS7上即便实现了也会调用,我想是通过底层的机制实现的, 因此这里大家可以注意下

1
2
3
4
5
6
7
8
9
10
11
+ (BOOL)canInitWithTask:(NSURLSessionTask *)task {
NSString *scheme = [[task.currentRequest URL] scheme];
if ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) {
//看看是否已经处理过了,防止无限循环
if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:task.currentRequest]) {
return NO;
}
return YES;
}
return NO;
}

通过session封装后的startLoding和stopLoading
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)startLoading {
NSMutableURLRequest* mutableReqeust = [[self request] mutableCopy];
//打标签,防止无限循环
[NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust];
// 封装custom headers
NSURLSessionConfiguration *configure = [NSURLSessionConfiguration defaultSessionConfiguration];
configure.protocolClasses = @[[CustomerURLProtocol class]];//注意这里的实现需要的是当前类的class类型 并非类本身
self.session = [NSURLSession sessionWithConfiguration:configure delegate:self delegateQueue:nil];
self.dataTask = [self.session dataTaskWithRequest:mutableReqeust];
[self.dataTask resume];
}

- (void) stopLoading {
// [self.connection cancel];
[self.dataTask cancel];
self.dataTask = nil;
}

另外NSURLSession提供了一个可以进行重定向的网络判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
completionHandler(request);//取消重定向的请求
// [self.client URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
}

- (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 disposition))completionHandler {
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
completionHandler(NSURLSessionResponseAllow);
}

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

总结:

两种方式基本都是通过实现了拦截相同的链接从而达到不在进行重定向的操作。由于NSURLConnection提供方法在iOS9之后就不在支持了,所以还是建议大家使用NSURLSession进行封装操作