UIWebView被WKWebView替換後在iOS12(不包含12)以下https雙向認證失敗的解決歷程總結

 

在日版iPhone5,10.3.3 系統上,有一些遺留問題,屬於另一個問題。

在這篇裏解決了 世界之大無奇不有之iOS網絡請求中HTTPBody內的鍵值對順序會導致請求失敗

  原生AF請求,第一個和登錄(未改動) 舊版UIWebview雙向認證 新版WKWebView雙向認證
iPhone5日版10.3.3真機 報錯(改完順序之後正常) 正常 改前報錯改後正常
10.3.1 - 11.4模擬器(無10.3.3) 正常 正常 改前報錯改後正常
12.4.1以上真機及模擬器 正常 正常 改前正常改後正常

 

在UIWebView時代,UIWebView的delegate沒有處理雙向認證的方法,需要在shouldStartLoadWithRequest方法中將請求攔截使用NSURLConnection來發起這個請求,NSURLConnection收到響應didReceiveResponse後取消請求,再將請求使用webview重新加載。

在WKWebView時代系統提供了處理挑戰(雙向認證)的方法,

/*! @abstract Invoked when the web view needs to respond to an authentication challenge.
 @param webView The web view that received the authentication challenge.
 @param challenge The authentication challenge.
 @param completionHandler The completion handler you must invoke to respond to the challenge. The
 disposition argument is one of the constants of the enumerated type
 NSURLSessionAuthChallengeDisposition. When disposition is NSURLSessionAuthChallengeUseCredential,
 the credential argument is the credential to use, or nil to indicate continuing without a
 credential.
 @discussion If you do not implement this method, the web view will respond to the authentication challenge with the NSURLSessionAuthChallengeRejectProtectionSpace disposition.
 */
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;

 

一般情況在這個方法裏面直接處理就可以,可是ios12及以上沒問題,以下都有問題。

 

猜想:是什麼原因導致的呢?

1.從bundle讀取文件轉成NSData發現ios10系統鼠標放上面是空的,但打印有值。多次嘗試不是這個問題

2.從p12文件讀取私鑰的方法是不是在ios10和12返回值不同。但怎麼看呢,打印數據沒有直接顯示的。

那就抓包試試看,直接不設代理抓取網卡上的底層包。參考  SSL/TSL雙向認證過程與Wireshark抓包分析

複習了一下三次握手

借圖

不是這個問題

3.ios10原生AFNet部分接口認證通過,部分不通過。多次嘗試,說明上面的方法沒問題

 

 

 

 

如果在wk使用UIWebView的那一套,NSURLConnection來攔截一下,didReceiveResponse後取消請求,wk繼續加載,發現不可行。

那didReceiveResponse後不取消,一直請求完成,然後wk直接加載NSURLConnection返回的數據,這樣行不行呢,終於取得了進展,認證可通過,但發現ajax發的請求會有問題。

 

上面幾種正規方法都試過了,不行那隻能私有api了,

而且沒有解決問題就不放中間代碼了。

 

先使用NSURLProtocol把WKWebView的所有請求攔截,在NSURLProtocol實現雙向認證,發現效果還可以,就是ajax的請求參數沒了,然後找到這麼一個庫“WKHookAjax”,解決了丟參問題,問題解決。

 

下面是最終解決問題的代碼

1.頁面創建後攔截請求的代碼

    [NSURLProtocol registerClass:[MyConnectionURLProtocol class]];
    Class cls = NSClassFromString(@"WKBrowsingContextController");
    SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
    if ([(id)cls respondsToSelector:sel]) {
        // 註冊http(s) scheme, 把 http和https請求交給 NSURLProtocol處理
        [(id)cls performSelector:sel withObject:@"http"];
        [(id)cls performSelector:sel withObject:@"https"];

    }

2.MyConnectionURLProtocol的代碼是

.h

#import <Foundation/Foundation.h>

@interface MyConnectionURLProtocol : NSURLProtocol

@end
.m


#import "MyConnectionURLProtocol.h"

#define protocolKey @"ConnectionProtocolKey"

@interface MyConnectionURLProtocol () <NSURLConnectionDataDelegate>

@property (nonatomic, strong) NSURLConnection * connection;
@property (nonatomic, assign) NSStringEncoding stringEncoding ;
@end

@implementation MyConnectionURLProtocol

//
//返回YES表示要攔截處理 走的第一個方法
//
+ (BOOL)canInitWithRequest:(NSMutableURLRequest *)request {
    NSString * url = request.URL.absoluteString;
    NSMutableURLRequest *mutableReq = [request mutableCopy];
    NSMutableDictionary *headers = [mutableReq.allHTTPHeaderFields mutableCopy];
    //防止已攔截的請求重複請求
    if ([headers objectForKey:@"Key1"]) {
        return NO;
    }
    NSLog(@"URL||||||||||   %@",request.URL);
    NSLog(@"HTTPMethod||||||||||   %@",request.HTTPMethod);
    NSLog(@"HTTPBody||||||||||  %@",[[NSString alloc]initWithData:request.HTTPBody encoding:NSUTF8StringEncoding] );
    NSLog(@"Header||||||||||   %@",request.allHTTPHeaderFields);
    // 如果url已http或https開頭,則進行攔截處理,否則不處理
    if ([url hasPrefix:@"http"] || [url hasPrefix:@"https"]) {
        return YES;
    }
    return NO;
} 
//
//修改請求頭 防止死循環
//
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    // 修改了請求的頭部信息
    NSMutableURLRequest *mutableRequest = [request mutableCopy];
    [mutableRequest setValue:@"AAAA" forHTTPHeaderField:@"Key1"];
    return mutableRequest;
}

//
//開始加載
//
- (void)startLoading{
    NSMutableURLRequest *request = [self.request mutableCopy];
    //
    //不是需要更改的頁面 則通過NSURLConnection去請求
    //
    self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
}
//
//取消請求
//
- (void)stopLoading {
    [self.connection cancel];
}

//
//#pragma mark - NSURLConnectionDelegate
//
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    NSString *str = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];;
    if (str.length < 1) {
        CFStringEncoding gbkEncoding =(unsigned int) CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);
        str =[[NSString alloc]initWithData:data encoding:gbkEncoding];
    }
    //
    //打印h5的代碼
    //
    NSLog(@"%@ %@",connection,str);
    [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];
}
//
//處理認證
//
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    
    NSURLCredential * credential;
    assert(challenge != nil);
    credential = nil;
 
    
    NSString *authenticationMethod = [[challenge protectionSpace] authenticationMethod];
    if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        
        NSString *host = challenge.protectionSpace.host;
        
        SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
        BOOL validDomain = false;
        NSMutableArray *polices = [NSMutableArray array];
        if (validDomain) {
            [polices addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)host)];
        }else{
            [polices addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
        }
        SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)polices);

        NSString *path = [[NSBundle mainBundle] pathForResource:@"XXXXXXX" ofType:@"cer"];
        NSData *certData = [NSData dataWithContentsOfFile:path];
        NSMutableArray *pinnedCerts = [NSMutableArray arrayWithObjects:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData), nil];
        SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCerts);
        
        credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
      
        
    } else {
       
        SecIdentityRef identity = NULL;
        SecTrustRef trust = NULL;
        NSString *p12 = [[NSBundle mainBundle] pathForResource:@"xxxxxxx" ofType:@"p12"];
        NSFileManager *fileManager = [NSFileManager defaultManager];
        if (![fileManager fileExistsAtPath:p12]) {

        }else{
            NSData *pkcs12Data = [NSData dataWithContentsOfFile:p12];
           
            if ([[self class] extractIdentity:&identity andTrust:&trust fromPKCS12Data:pkcs12Data]) {
                SecCertificateRef certificate = NULL;
                SecIdentityCopyCertificate(identity, &certificate);
                const void *certs[] = {certificate};
                CFArrayRef certArray = CFArrayCreate(kCFAllocatorDefault, certs, 1, NULL);
                credential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray *)certArray persistence:NSURLCredentialPersistencePermanent];
            }
        }
    }
   
    [challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
}

+ (BOOL)extractIdentity:(SecIdentityRef *)outIdentity andTrust:(SecTrustRef *)outTrust fromPKCS12Data:(NSData *)inPKCS12Data {
    
    OSStatus securityErr = errSecSuccess;

    NSDictionary *optionsDic = [NSDictionary dictionaryWithObject:@"XXXXXXXXX" forKey:(__bridge id)kSecImportExportPassphrase];
    CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
    
    
    securityErr = SecPKCS12Import((__bridge CFDataRef)inPKCS12Data, (__bridge CFDictionaryRef)optionsDic, &items);
    if (securityErr == errSecSuccess) {
        CFDictionaryRef mineIdentAndTrust = CFArrayGetValueAtIndex(items, 0);
        const void *tmpIdentity = NULL;
        tmpIdentity = CFDictionaryGetValue(mineIdentAndTrust, kSecImportItemIdentity);
        *outIdentity = (SecIdentityRef)tmpIdentity;
        const void *tmpTrust = NULL;
        tmpTrust = CFDictionaryGetValue(mineIdentAndTrust, kSecImportItemTrust);
        *outTrust = (SecTrustRef)tmpTrust;
    }else{
      
        return false;
    }
    return true;
}

@end

3.hook ajax  這裏就不放WKHookAjax的下載地址了

    WKUserContentController *wkUController = [[WKUserContentController alloc] init];
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    config.userContentController = wkUController;
//注意 一定要寫在上面那句的下面
    [config.userContentController imy_installHookAjax];

 

4.如果有不需要認證的,不需要hook ajax的 ,可以在適當的地方取消攔截 取消hook

 

 

疑問:NSURLConnection和NSURLSession和WKWebView這三個處理雙向認證有什麼區別,爲什麼只有NSURLConnection纔是最穩定的。

那部分機型原生請求出錯是不是可以把NSURLSession換成NSURLConnection呢?

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章