iOS使用Webview加载https网页
项目新的需求需要在客户端中嵌入网页,以前的项目中也做过类似的功能,感觉不会有什么困难,使用WebView加载指定的URL就行了。但这次我们需要加载https页面,由于没有相关的实战经验,在整个开发过程中踩了不少的坑,现在给大家分享一下。
首先我很2的像以前一样直接加载URL,显然是加载不出来内容的。然后开始Google,首先看到可以通过私有API中的setAllowsAnyHTTPSCertificate:forHost方法但这有被拒的风险。还有一种是通过实现自己的NSURLProtocol协议,然后拦截需要拦截的请求,在请求代理中进行证书验证。然后在把请求的结果通过NSURLProtocol的self.client URLProtocol:didLoadData:方法返回给WebView。NSURLProtocol的具体使用需要实现一下几个方法:
1
2
3
+ ( BOOL ) canInitWithRequest: ( NSURLRequest * ) request {
return [ request . URL . absoluteString isEqualToString: @"https://" ];
}
canInitWithRequest方法如果是需要自己实现的请求就return YES 否则 return NO。
1
2
3
4
5
+ ( NSURLRequest * ) canonicalRequestForRequest: ( NSURLRequest * ) request {
//返回规范化的请求,直接返回当前的Request就可以。
NSMutableURLRequest * cdnRequest = [ request mutableCopy ];
return cdnRequest ;
}
1
2
3
- ( void ) startLoading {
//具体的加载实现
}
1
2
3
- ( void ) stopLoading {
//停止
}
下面看看我们具体的实现:
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
//
// MyWebviewProtocol.m
// Cloay
//
// Created by cloay on 16/6/14.
// Copyright © 2016年 Cloay. All rights reserved.
//
#import "MyWebviewProtocol.h"
#import "AFSecurityPolicy.h"
@interface MyWebviewProtocol () < NSURLSessionDataDelegate >
@property ( nonatomic , strong ) NSURLSession * session ;
@end
@implementation MyWebviewProtocol
+ ( BOOL ) canInitWithRequest: ( NSURLRequest * ) request {
NSString * urlStr = request . URL . absoluteString ;
CLog ( @"----origin request url = %@" , urlStr );
NSRange range = [ urlStr rangOfString: @"https://" ];
//这里是我们的拦截逻辑,需要自己实现的请求就return YES
return range . length > 0 ;
}
+ ( NSURLRequest * ) canonicalRequestForRequest: ( NSURLRequest * ) request {
NSMutableURLRequest * myRequest = [ request mutableCopy ];
return myRequest ;
}
- ( void ) startLoading {
if ( ! self . session ) {
NSOperationQueue * queue = [[ NSOperationQueue alloc ] init ];
self . session = [ NSURLSession sessionWithConfiguration: [ NSURLSessionConfiguration defaultSessionConfiguration ] delegate: self delegateQueue: queue ];
}
NSURLSessionDataTask * task = [ self . session dataTaskWithRequest: [ TCWebviewProtocol canonicalRequestForRequest: self . request ]];
[ task resume ];
}
- ( void ) stopLoading {
[ self . session invalidateAndCancel ];
[ self . session finishTasksAndInvalidate ];
}
#pragma mark NSURLSession Delegate
- ( void ) URLSession: ( NSURLSession * ) session task: ( NSURLSessionTask * ) task willPerformHTTPRedirection: ( NSHTTPURLResponse * ) response
newRequest: ( NSURLRequest * ) request
completionHandler: ( void ( ^ )( NSURLRequest * __nullable )) completionHandler {
[ self . client URLProtocol: self wasRedirectedToRequest: request redirectResponse: response ];
}
- ( void ) URLSession: ( NSURLSession * ) session didReceiveChallenge: ( NSURLAuthenticationChallenge * ) challenge completionHandler: ( void ( ^ )( NSURLSessionAuthChallengeDisposition , NSURLCredential * )) completionHandler {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling ;
NSURLCredential * credential = nil ;
if ([ challenge . protectionSpace . authenticationMethod isEqualToString: NSURLAuthenticationMethodServerTrust ]){
AFSecurityPolicy * securityPolicy = [ AFSecurityPolicy policyWithPinningMode: AFSSLPinningModeCertificate ];
# if DEBUG
securityPolicy . allowInvalidCertificates = YES ;
securityPolicy . validatesDomainName = NO ;
# endif
if ([ securityPolicy evaluateServerTrust: challenge . protectionSpace . serverTrust forDomain: challenge . protectionSpace . host ]) {
credential = [ NSURLCredential credentialForTrust: challenge . protectionSpace . serverTrust ];
if ( credential ) {
disposition = NSURLSessionAuthChallengeUseCredential ;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge ;
}
}
completionHandler ( disposition , credential );
}
- ( 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 ];
}
- ( void ) URLSession: ( NSURLSession * ) session task: ( NSURLSessionTask * ) task didCompleteWithError: ( NSError * ) error {
if ( error ) {
[ self . client URLProtocol: self didFailWithError: error ];
} else {
[ self . client URLProtocolDidFinishLoading: self ];
}
}
@end
我们的项目兼容iOS7以上系统,所以这里我们就使用了NSURLSession来实现具体的请求。通过委托的URLSession:didReceiveChallenge:completionHandler:方法进行证书的验证。通过这种方式看似很好的解决了我们的问题,也没有什么难度。然而在我们后续的测试和使用WKWebview优化的过程中相关坑陆续的蹦出来了,页面怎么都加载不出来了。把我都快坑炸了。。。
我们发现的坑
开始调试时通过这种方式在iOS7和iOS9中没有什么问题,所有页面正常显示。后来我们为了性能在iOS8以上系统使用WKWebview来实现我们的功能。WKWebView是iOS8以后出的新的性能更优的WebView,虽然WKWebview的请求不能通过NSURLProtocol这种方式拦截,但可以直接通过WKNavigationDelegate中的webView:didReceiveChallenge:completionHandler:方法完成我们的需求,这样更简单。然后我们就马不停蹄的在iOS8以后的系统中替换成了WKWebView,然而事实证明我们是Too young了。在调试时我们发现iOS8系统死活不调用webView:didReceiveChallenge:completionHandler:方法,后来发现这是iOS8的一个bug( Server trust authentication challenges aren’t sent to the navigation delegate ),直到iOS9才修复。
接着我们又改为在iOS9使用WKWebView,iOS9以下继续使用UIWebView。悲剧的是在接下来的调试中我们又发现了一个问题。我们页面中需要登录,做法是客户端中用户登录后把token传给页面,让页面做一次自动登录。调试时在iOS7中一切正常,然而在iOS8中一直登录不成功,页面一直重定向到一个登录页面。这个问题纠结了好几天后来发现我们没有保持cookie,我们的NSURLSession的配置使用ephemeralSessionConfiguration,看文档发现ephemeralSessionConfiguration不保持cookie的。好吧,又2了。。。
总结
客户端嵌入https网页时,在iOS8及一下还是只能使用UIWebView,通过实现NSURLProtocol自己实现网络请求进行证书验证。在iOS9系统中直接使用WKWebView就能很好的满足常见的需求。
参考链接: