iOS自签名HTTPS证书单向校验方案

原文:iOS自签名HTTPS证书单向校验方案

前言

这两天公司选所谓的先进个人,结果也是我最满意的,没选上但能得到自己身边的伙伴对自己的认可,很开心,感冒也好了很多!虽然知道大家选我,只是对我工作层面的认可,与人品无关,但我宁愿伙伴能对我的人品层面也有同等认可。

毕业已近两年,出来工作时间总算起来,也有快三年了。从开始跟着师傅,到慢慢独立,再到带新伙伴一起做项目,这一路走来,有过无助、有过怀疑,有过为伙伴“甘愿独自离去”的冲动,有过低级趣味的诱惑,有过感动,有过坚定,说我傻B也好,说我怎样也罢,我都全盘接受。

说实话,有时也会矛盾。我是一个“虚心听取别人意见”的人,别人的建议我反思后,除了性格使然部分,其他大部分我都会调整,当然你也可以理解为没有主见,设计、测试怎么说就怎么改,领导安排工作也尽量去做。慢慢的对自己的工作方式也有过疑问,这样做是否合适?也有贵人给我提过一些东西,说让我学会表达自己,为此,我也做出些自己的改变,在技术方面,我开始表达自己的想法,不过有时会不注意表达的方法和方式,也会给领导给自己造成些许困惑。

不管怎样,猴年即将过去,这恐怕是年前最后一次项目上线了,给大家分享下前几天HTTPS证书校验的一些东西。
说好的写篇《iOS自签名HTTPS证书单向校验方案》呢,又扯了这么多闲篇,对大家不住。

HTTPS简析

虽说在去年圣诞节前夕苹果发出公告要推迟ATS适配截止时间,但既然它在iOS9.0始推出App Transport Security,适配HTTPS是早晚的事,总要做好技术储备。以鄙人浅显的技术而言,我觉得iOS APP适配HTTPS主要涉及三方面适配:

  • 普通网络请求;
  • H5页面加载;
  • SDWebImage加载HTTPS图片(一般公司测试库会用到自签名证书)。

所谓HTTPS,即HTTP+SSL/TSL,底部也就是在HTTP协议层和TCP/IP协议层之间添加安全传输层协议SSL,从而达到对HTTP数据包加密传输的目的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sequenceDiagram
participant Client
participant Server
Client->>Server: 以明文传输数据,主要有客户端支持的SSL版本等客户端支持的加密信息
Server-->>Server: 服务端选择加密方式
Server-->>Client: 服务端给客户端返回SSL版本、随机数等信息
Client->>Client: 校验服务端证书是否合法、产生随机数
Client->>Server: 使用服务端随机数加密数据发给服务端
Server-->>Server: 服务端使用私钥解密客户端数据
Server-->>Client: 使用收到的随机数,加密数据,返回给客户端
Client->>Client: 客户端使用公钥解密数据

简书不支持MarkDown高级语法,上面的MarkDown文本对应下图:

HTTPS sequenceDiagram.png

iOS适配HTTPS注意点

  • 网络请求NSURLConnection/NSURLSession
  • H5页面适配WKWebView/UIWebView
  • SDWebImage图片加载适配

网络请求部分

NSURLConnection适配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
{
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
}
else
{
if ([challenge previousFailureCount] == 0)
{
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
else
{
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
}
}

NSURLSession适配

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
/** disposition:如何处理证书
NSURLSessionAuthChallengeUseCredential 使用证书
NSURLSessionAuthChallengePerformDefaultHandling 忽略证书 默认的做法
NSURLSessionAuthChallengeCancelAuthenticationChallenge 取消请求,忽略证书
NSURLSessionAuthChallengeRejectProtectionSpace 拒绝,忽略证书
*/
#pragma mark - NSURLSessionDelegate代理方法 HTTPS ---开始---
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler
{
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
// 判断服务器返回的证书是否是服务器信任的
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
{
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
if (credential)
{
disposition = NSURLSessionAuthChallengeUseCredential; // 使用证书
}
else
{
disposition = NSURLSessionAuthChallengePerformDefaultHandling; // 忽略证书 默认的做法
}
}
else
{
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge; // 取消请求,忽略证书
}
if (completionHandler)// 安装证书
{
completionHandler(disposition, credential);
}
}

H5页面适配

基于WKWebView的H5页面HTTPS适配
1
2
3
4
5
6
7
8
9
10
#pragma mark: WKWebView的https配置
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler
{
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
{
NSURLCredential *card = [[NSURLCredential alloc]initWithTrust:challenge.protectionSpace.serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential,card);
}
}
基于UIWebView的H5页面HTTPS适配

详情见:Stretch’s stackoverflow答案

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
#pragma mark - Webview delegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
{
NSLog(@"Did start loading: %@ auth:%d", [[request URL] absoluteString], _authenticated);
if (!_authenticated)
{
_authenticated = NO;
_urlConnection = [[NSURLConnection alloc] initWithRequest:_request delegate:self];
[_urlConnection start];
return NO;
}
return YES;
}
#pragma mark - NURLConnection delegate
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
{
NSLog(@"WebController Got auth challange via NSURLConnection");
if ([challenge previousFailureCount] == 0)
{
_authenticated = YES;
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
[challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
} else
{
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
{
NSLog(@"WebController received response via NSURLConnection");
// remake a webview call now that authentication has passed ok.
_authenticated = YES;
[_web loadRequest:_request];
// Cancel the URL connection otherwise we double up (webview + url connection, same url = no good!)
[_urlConnection cancel];
}
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}

SDWebImage加载HTTPS图片适配

低版本的SDWebImage是对NSURLConnection做的封装,新版本是对NSURLSession做的封装,适配HTTPS图片,记得确定自己使用的SDWebImage版本的SDWebImageDownloaderOperation类是否做过校验。
如果SDWebImage使用的是NSURLConnection看看文件是否有:

1
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge

否则查看SDWebImageDownloaderOperation文件中是否有

1
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler

确定好之后,如果所有图片均是HTTPS的,那么我们可以直接修改UIImageView+WebCache文件

1
2
3
4
5
6
7
8
9
10
11
// HTTPS配置问题
- (void)sd_setImageWithURL:(nullable NSURL *)url
{
// [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
[self sd_setImageWithURL:url placeholderImage:nil options:SDWebImageAllowInvalidSSLCertificates progress:nil completed:nil]; // HTTPS配置问题
}
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder
{
// [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
[self sd_setImageWithURL:url placeholderImage:placeholder options:SDWebImageAllowInvalidSSLCertificates progress:nil completed:nil]; // HTTPS配置问题
}

其实质就是将option设置为SDWebImageAllowInvalidSSLCertificates,看SDWebImageDownloaderOperation文件你会发现,SDWebImage采用直接忽略证书验证的方式加载的,所以设置SDWebImageAllowInvalidSSLCertificates才有效。
SDWebImageDownloaderloaderOperation校验忽略.png
如有个别特殊情况,要支持某些HTTP的图片,按照上面方法适配后,还需在plist的Exception Domains添加响应配置,详情见苹果APP接入HTTPS延迟截止时间是妥协吗