前言
公司项目由HTTP转为HTTPS,需要对网络请求进行自建证书验证。主要是AFNetWorking
、SDWebImage
和WKWebView
。
HTTP与HTTPS
AFNetWorking
详情参考AFNetworking之于https认证。之后的主要是使用AFNetWorking
的方法进行验证。
- 自建证书验证工具方法
static AFSecurityPolicy *securityPolicyShare = NULL;
@implementation HTTPSAuthenticationChallenge
+(AFSecurityPolicy *)customSecurityPolicy {
// 保证证书验证初始化一次
if (securityPolicyShare != NULL) {
return securityPolicyShare;
}
// 加载证书
NSString *crtBundlePath = [[NSBundle mainBundle] pathForResource:@"Res" ofType:@"bundle"];
NSBundle *resBundle = [NSBundle bundleWithPath:crtBundlePath];
NSSet<NSData *> *cerDataSet = [AFSecurityPolicy certificatesInBundle:resBundle];
// AFSSLPinningModeCertificate使用证书验证模式
securityPolicyShare = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:cerDataSet];
return securityPolicyShare;
}
+ (void)authenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
// 获取服务器证书信息
SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
NSURLCredential *credential = nil;
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
// 基于客户端的安全策略来决定是否信任该服务器,不信任的话,也就没必要响应验证
if ([[HTTPSAuthenticationChallenge customSecurityPolicy] evaluateServerTrust:serverTrust forDomain:nil]) {
// 创建挑战证书(注:挑战方式为UseCredential和PerformDefaultHandling都需要新建证书)
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
// credential存在时使用证书验证
// credential为nil时忽略证书,默认的处理方式
disposition = credential == nil ? NSURLSessionAuthChallengePerformDefaultHandling : NSURLSessionAuthChallengeUseCredential;
} else {
// 忽略证书,取消请求
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
if (completionHandler) {
completionHandler(disposition,credential);
}
}
@end
SDWebImage
- 创建
SDWebImageDownloader
的分类,在分类中进行验证
SDWebImageDownloader
是SDWebImageView
下载图片的核心类,在分类中重写NSURLSession
的代理方法didReceiveChallenge
进行自建证书的验证
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
[HTTPSAuthenticationChallenge authenticationChallenge:challenge completionHandler:completionHandler];
}
WKWebView
-
WKWebView
的验证常规情况下在navigationDelegate
的didReceiveAuthenticationChallenge
中进行
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler{
[HTTPSAuthenticationChallenge authenticationChallenge:challenge completionHandler:completionHandler];
}
-
非常规情况,假如
WKWebView
展示html
里有大量图片,并且用户点击图片时进行展示。- 不考虑效率
这种情况下可以在WKWebView
加载完成后使用JS
注入的方式获取html
里图片的src
,然后利用SDWebImage
进行预下载。这样就会造成图片两次下载(网页加载时下载图片和用户点击展示图片时进行下载),浪费流量。但此方法可以让我们正常的在navigationDelegate
的didReceiveAuthenticationChallenge
中进行证书验证。
- (void)imagesPrefetcher:(WKWebView *)webView { static NSString * const jsGetImages = @"function getImages(){\ var objs = document.getElementsByTagName(\"img\");\ var imgScr = '';\ for(var i=0;i<objs.length;i++){\ imgScr = imgScr + objs[i].src + '+';\ };\ return imgScr;\ };"; [webView evaluateJavaScript:jsGetImages completionHandler:nil]; [webView evaluateJavaScript:@"getImages()" completionHandler:^(id _Nullable result, NSError * _Nullable error) { NSArray *urlArray = [NSMutableArray arrayWithArray:[result componentsSeparatedByString:@"+"]]; //urlResurlt 就是获取到得所有图片的url的拼接;mUrlArray就是所有Url的数组 CGLog(@"Image--%@",urlArray); NSMutableArray<NSURL *> *tempURLs = [NSMutableArray array]; for (NSString* urlStr in urlArray) { NSURL *url = [NSURL URLWithString:urlStr]; if (url) { [tempURLs addObject:url]; } } [[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:tempURLs]; }]; }
- 考虑效率
使用NSURLProtocol
进行拦截html
加载图片的请求,由自己来进行请求并缓存图片,这样可以避免在用点击时展示图片时再次请求,直接使用缓存图片进行展示就行了。虽然节省了流量,提高了效率,但是我们不能够navigationDelegate
的didReceiveAuthenticationChallenge
中进行证书验证了,这个代理方法不会走的,我们要考虑在NSURLProtocol
中进行验证。
/** WhiteList validation */ @interface NSURLRequest (WhiteList) - (BOOL)isInWhiteList; - (BOOL)is4CGTNPictureResource; - (BOOL)is4CGTNResource; @end @implementation NSURLRequest (WhiteList) - (BOOL)isInWhiteList { // 手机端非CGTN的第三方不需要验证,也是属于白名单的 BOOL isMobileRequest = [self.URL.host isEqualToString:@"events.appsflyer.com"] || [self.URL.host isEqualToString:@"ssl.google-analytics.com"]; if (isMobileRequest) { return YES; } // webView NSArray<NSString *> *whiteListStr = [[NSUserDefaults standardUserDefaults] objectForKey:@"kWhiteList"]; if (whiteListStr.count == 0) { return YES; } NSSet *whiteListSet = [NSSet setWithArray:whiteListStr]; // requestURL --- scheme + host NSString *requestURL = [[self.URL.scheme stringByAppendingString:@"://"] stringByAppendingString:self.URL.host]; BOOL isInList = [whiteListSet containsObject:requestURL]; // 在白名单的使用系统默认处理 // 不在白名单的进行拦截验证 return isInList; } - (BOOL)is4CGTNResource { NSURLComponents *components = [[NSURLComponents alloc] initWithURL:self.URL resolvingAgainstBaseURL:YES]; BOOL isCGTNResource = [components.host containsString:@"cgtn.com"]; if (isCGTNResource) { return YES; } return NO; } - (BOOL)is4CGTNPictureResource { NSURLComponents *components = [[NSURLComponents alloc] initWithURL:self.URL resolvingAgainstBaseURL:YES]; BOOL isCGTNResource = [components.host containsString:@"cgtn.com"]; NSString *extensionName = self.URL.pathExtension; BOOL canHandle = [extensionName containsString:@"png"] || [extensionName containsString:@"jpg"] || [extensionName containsString:@"jpeg"] || [extensionName containsString:@"gif"]; if (canHandle && isCGTNResource) { return YES; } return NO; } @end static NSString *const handledKey = @"com.cgtn.www"; @interface CGTNURLProtocol ()<NSURLSessionDelegate> /** 用于图片链接交由SDWebImage 管理 */ @property (strong, nonatomic) id<SDWebImageOperation> operation; /** 其他非图片任务交由 Cache 管理, task任务 */ @property (strong, nonatomic) NSURLSessionDataTask *dataTask; /** 是否为图片链接 */ @property (nonatomic) BOOL isPicture; @end @implementation CGTNURLProtocol + (BOOL)canInitWithRequest:(NSURLRequest *)request { if (!request.URL || [self propertyForKey:handledKey inRequest:request]) { return NO; } CGLog(@"canInitWithRequest----%@",request.URL); // 白名单中的链接不进行拦截. 默认外部处理 // CGTN 的资源强制进行验证 return !request.isInWhiteList || request.is4CGTNResource; } + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; } + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { return [super requestIsCacheEquivalent:a toRequest:b]; } - (void)startLoading { [self.class setProperty:@(YES) forKey:handledKey inRequest:self.request.mutableCopy]; // 拦截到的请求必须为 HTTPS if ([self.request.URL.scheme isEqualToString:@"http"]) { NSError *error = [NSError CGTNErrorWithCode:NSURLErrorAppTransportSecurityRequiresSecureConnection description:@"Deny HTTP request"]; [self.client URLProtocol:self didFailWithError:error]; return; } self.isPicture = self.request.is4CGTNPictureResource; if (self.isPicture) { __weak typeof(self) weakSelf = self; self.operation = [SDWebImageManager.sharedManager loadImageWithURL:self.request.URL options:SDWebImageRetryFailed progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { if (error) { [weakSelf.client URLProtocol:weakSelf didFailWithError:error]; return; } NSData *imageData = data; if (!data && image) { imageData = image.sd_imageData; } if (!imageData) { [weakSelf.client URLProtocolDidFinishLoading:weakSelf]; return; } NSURLResponse *response = [[NSURLResponse alloc] initWithURL:imageURL MIMEType:nil expectedContentLength:imageData.length textEncodingName:nil]; [weakSelf.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed]; [weakSelf.client URLProtocol:weakSelf didLoadData:imageData]; [weakSelf.client URLProtocolDidFinishLoading:weakSelf]; }]; return; } // 非图片请求 NSURLSessionConfiguration *config = NSURLSessionConfiguration.defaultSessionConfiguration; NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; self.dataTask = [session dataTaskWithRequest:self.request]; [self.dataTask resume]; } - (void)stopLoading { if (self.isPicture) { [self.operation cancel]; return; } [self.dataTask cancel]; } #pragma mark - NSURLSessionDelegate - (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]; } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler { completionHandler(proposedResponse); } - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler { [HTTPSAuthenticationChallenge authenticationChallenge:challenge completionHandler:completionHandler]; } @end
- 不考虑效率