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

96
仁伯
2017.01.13 18:03* 字数 967

前言

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

毕业已近两年,出来工作时间总算起来,也有快三年了。从开始跟着师傅,到慢慢独立,再到带新伙伴一起做项目,这一路走来,有过无助、有过怀疑,有过为伙伴“甘愿独自离去”的冲动,有过低级趣味的诱惑,有过感动,有过坚定,说我傻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数据包加密传输的目的,

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适配

- (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适配

/** 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适配
#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答案

#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看看文件是否有:

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

否则查看SDWebImageDownloaderOperation文件中是否有

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

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

- (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延迟截止时间是妥协吗

iOS网络篇
Web note ad 1