1. 程式人生 > >iOS NSURLProtocol詳解和應用

iOS NSURLProtocol詳解和應用

com string handle gist mutable 註冊 header rec canonical

  問題:因dns發生域名劫持 需要手動將URL請求的域名重定向到指定的IP地址   最近在項目裏由於電信那邊發生dns發生域名劫持,因此需要手動將URL請求的域名重定向到指定的IP地址,但是由於請求可能是通過NSURLConnection,NSURLSession或者AFNetworking等方式,因此要想統一進行處理,一開始是想通過Method Swizzling去hook cfnetworking底層方法,後來發現其實有個更好的方法--NSURLProtocol。

NSURLProtocol

NSURLProtocol能夠讓你去重新定義蘋果的URL加載系統 (URL Loading System)的行為,URL Loading System裏有許多類用於處理URL請求,比如NSURL,NSURLRequest,NSURLConnection和NSURLSession等,當URL Loading System使用NSURLRequest去獲取資源的時候,它會創建一個NSURLProtocol子類的實例,你不應該直接實例化一個NSURLProtocol,NSURLProtocol看起來像是一個協議,但其實這是一個類,而且必須使用該類的子類,並且需要被註冊。


使用場景

不管你是通過UIWebView, NSURLConnection 或者第三方庫 (AFNetworking, MKNetworkKit等),他們都是基於NSURLConnection或者 NSURLSession實現的,因此你可以通過NSURLProtocol做自定義的操作。

  • 重定向網絡請求
  • 忽略網絡請求,使用本地緩存
  • 自定義網絡請求的返回結果
  • 攔截圖片加載請求,轉為從本地文件加載
  • 一些全局的網絡請求設置
  • 快速進行測試環境的切換
  • 過濾掉一些非法請求
  • 網絡的緩存處理(H5離線包 和 網絡圖片緩存)
  • 可以攔截UIWebView,基於系統的NSURLConnection或者NSURLSession進行封裝的網絡請求。目前WKWebView無法被NSURLProtocol攔截。
  • 當有多個自定義NSURLProtocol註冊到系統中的話,會按照他們註冊的反向順序依次調用URL加載流程。當其中有一個NSURLProtocol攔截到請求的話,後續的NSURLProtocol就無法攔截到該請求。

攔截網絡請求

子類化NSURLProtocol並註冊

@interface CustomURLProtocol : NSURLProtocol
@end

然後在application:didFinishLaunchingWithOptions:方法中註冊該CustomURLProtocol,一旦註冊完畢後,它就有機會來處理所有交付給URL Loading system的網絡請求。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    //註冊protocol
    [NSURLProtocol registerClass:[CustomURLProtocol class]];
    return YES;
}

實現CustomURLProtocol

#import "CustomURLProtocol.h"

static NSString* const URLProtocolHandledKey = @"URLProtocolHandledKey";

@interface CustomURLProtocol ()<NSURLConnectionDelegate>
@property (nonatomic, strong) NSURLConnection *connection;

@end
@implementation CustomURLProtocol

//這裏面寫重寫的方法

@end

註冊好了之後,現在可以開始實現NSURLProtocol的一些方法:

  • +canInitWithRequest:
    這個方法主要是說明你是否打算處理對應的request,如果不打算處理,返回NO,URL Loading System會使用系統默認的行為去處理;如果打算處理,返回YES,然後你就需要處理該請求的所有東西,包括獲取請求數據並返回給 URL Loading System

    網絡數據可以簡單的通過NSURLConnection去獲取,而且每個NSURLProtocol對象都有一個NSURLProtocolClient實例,可以通過該client將獲取到的數據返回給URL Loading System

    這裏有個需要註意的地方,想象一下,當你去加載一個URL資源的時候,URL Loading System會詢問CustomURLProtocol是否能處理該請求,你返回YES,然後URL Loading System會創建一個CustomURLProtocol實例然後調用NSURLConnection去獲取數據,然而這也會調用URL Loading System,而你在+canInitWithRequest:中又總是返回YES,這樣URL Loading System又會創建一個CustomURLProtocol實例導致無限循環。我們應該保證每個request只被處理一次,可以通過+setProperty:forKey:inRequest:標示那些已經處理過的request,然後在+canInitWithRequest:中查詢該request是否已經處理過了,如果是則返回NO。
    + (BOOL)canInitWithRequest:(NSURLRequest *)request
    {
      //只處理http和https請求 返回NO默認讓系統去處理
        NSString *scheme = [[request URL] scheme];
        if ( ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
         [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame))
        {
            //看看是否已經處理過了,防止無限循環 根據業務來截取 
            if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
                return NO;
            }
        
             //還要在這裏截取DSN解析請求中的鏈接 判斷攔截域名請求的鏈接如果是返回NO
             if (判斷攔截域名請求的鏈接) {
                return NO;
            }
            
            return YES;
        }
        return NO;
    }

  • +canonicalRequestForRequest 如果沒有特殊需求,直接返回request就可以了, 可以在開始加載中startLoading方法中 修改request,比如添加header,修改host,請求重定向等
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
  return request;
 } 

  • +requestIsCacheEquivalent:toRequest:
    主要判斷兩個request是否相同,如果相同的話可以使用緩存數據,通常只需要調用父類的實現。
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
    return [super requestIsCacheEquivalent:a toRequest:b];
}

  • -startLoading -stopLoading
    這兩個方法主要是開始和取消相應的request,而且需要標示那些已經處理過的request。
    - (void)startLoading
    {
        NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
        //標示改request已經處理過了,防止無限循環
        [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust];
        //重定向
        self.connection = [NSURLConnection connectionWithRequest:[self dealDNS: mutableReqeust] delegate:self];
    }
    
    - (void)stopLoading
    {
        [self.connection cancel];
      self.connection = nil ;
    }
    
    //解決劫持 重定向到ip地址
    - (NSMutableURLRequest *)dealDNS:(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;
        }
    
         //根據當前host(如baidu.com)請求獲取到IP(如172.128.3.3)  並替換到URL中
        //定向到IP 改IP需要根據 host 去請求獲取到
        NSString *ip = 根據 host請求獲取; // (如baidu.com)同步請求獲取到IP(如172.128.3.3)  註意??這個請求的鏈接需要在canInitWithRequest裏面過濾掉
        
        // 替換域名
        //URL:https://www.baidu.com   替換成了https://172.128.3.3
        NSString *urlString = [originUrlString stringByReplacingCharactersInRange:hostRange withString:ip];
        NSURL *url = [NSURL URLWithString:urlString];
        request.URL = url;
    
         //給改IP設置Cookie
         [CookieManager setWebViewCookieForDomain:ipUrl.host];
    
        return request;
    }

  • NSURLConnectionDataDelegate方法
    在處理網絡請求的時候會調用到該代理方法,我們需要將收到的消息通過client返回給URL Loading System
- (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];
}

//解決發送IP地址的HTTPS請求 證書驗證
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{

    if (!challenge) {
        return;
    }
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
    {
        //構造一個NSURLCredential發送給發起方
        NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];

        [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];

    } else {

       //對於其他驗證方法直接進行處理流程
        [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];

    }

}

註意??:這裏面在 手動將URL請求的域名重定向到指定的IP地址 去請求的時候需要 對HTTPS請求校驗證書



iOS NSURLProtocol詳解和應用