自己动手写一个 iOS 网络请求库(五)——设置 SSL 钢钉

代码示例:https://github.com/johnlui/Swift-On-iOS/blob/master/BuildYourHTTPRequestLibrary

开源项目:Pitaya,适合大文件上传的 HTTP 请求库:https://github.com/johnlui/Pitaya

这个系列的文章本已终结,现在续上,就是为了一个未来大家一定会越来越需要的功能:设置 SSL 证书钢钉。

说起来这个功能也很简单,在我们调用 HTTPS 协议的时候,事先把 SSL 证书存到 App 本地,然后在每次请求的时候都进行一次验证,避免中间人攻击(Man-in-the-middle attack)。同时,这个功能也是我们使用自签名证书时候必须的,因为系统默认会拒绝我们自己签名的不受信任的证书,导致连接失败。

废话不多说,我们进入正题。

证书获取

NSURLSession 支持 cer 格式的证书文件,而 Apache 和 Nginx 默认的证书都是 crt 格式,我们需要双击将其安装到系统中,再使用钥匙串 App 将这个证书导出为 cer 格式即可。

Image

Image

开搞

经过查询资料,发现 NSURLSession 提供了 SSL 证书处理的代理方法,我们需要对我们的 NetworkManager 类进行一点点改造。

自定义 session

如果想要调用到我们想要的代理方法,需要我们自定义一下 NSURLSession 对象:

var session: NSURLSession!
... ...

init(... ...) {
    ... ...
    super.init()
    self.session = NSURLSession(configuration: NSURLSession.sharedSession().configuration, delegate: self, delegateQueue: NSURLSession.sharedSession().delegateQueue)
}

实现代理

由于上面我们把 NSURLSession 的代理设置成了 self,所以现在我们要让 NetworkManager 类实现 NSURLSessionDelegate 这个 protocol。又由于 NSURLSessionDelegate 继承自 NSObjectProtocol,所以我们需要让 NetworkManager 继承自 NSObject 类:

class NetworkManager: NSObject, NSURLSessionDelegate {
... ...

实现代理方法

接下来我们就通过实现 SSL 证书检查的代理方法来干预网络请求了。

增加两个成员变量:

var localCertData: NSData!
var sSLValidateErrorCallBack: (() -> Void)?

增加设置他们的函数:

func addSSLPinning(LocalCertData data: NSData, SSLValidateErrorCallBack: (()->Void)? = nil) {
    self.localCertData = data
    self.sSLValidateErrorCallBack = SSLValidateErrorCallBack
}

实现代理方法,介入网络请求:

@objc func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
    if let localCertificateData = self.localCertData {
        if let serverTrust = challenge.protectionSpace.serverTrust,
            certificate = SecTrustGetCertificateAtIndex(serverTrust, 0),
            remoteCertificateData: NSData = SecCertificateCopyData(certificate) {
                if localCertificateData.isEqualToData(remoteCertificateData) {
                    let credential = NSURLCredential(forTrust: serverTrust)
                    challenge.sender?.useCredential(credential, forAuthenticationChallenge: challenge)
                    completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, credential)
                } else {
                    challenge.sender?.cancelAuthenticationChallenge(challenge)
                    completionHandler(NSURLSessionAuthChallengeDisposition.CancelAuthenticationChallenge, nil)
                    self.sSLValidateErrorCallBack?()
                }
        } else {
            NSLog("Get RemoteCertificateData or LocalCertificateData error!")
        }
    } else {
        completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, nil)
    }
}

至此,检测 SSL 证书的功能就做完了。接下来我们检验成果。

检验成果

『Thus, programs must be written for people to read, and only incidentally for machines to execute.』

——《Structure and Interpretation of Computer Programs 》 Harold Abelson

『代码是写给人看的,只是恰好能运行。』这句话出自大名鼎鼎的 SICP,出处:https://mitpress.mit.edu/sicp/front/node3.html 

在搞完了这个功能之后,我突然发现我好像被 Alamofire 的 API 设计给带偏了:写起来方便是最不重要的,便于使用者理解才是最重要的。所以我打算杀掉所有疑似假装是奇技淫巧的集合型 API,改由纯粹的 构造对象->修改对象->发起请求 模式,降低使用者的理解成本。

我使用我的网站 lvwenhan.com  的证书来进行此次验证:

let network = NetworkManager(url: "https://lvwenhan.com/", method: "GET") { (data, response, error) -> Void in
    if let _ = error {
        NSLog(error.description)
    } else {
        print("证书正确!")
    }
}
let certData = NSData(contentsOfFile: NSBundle.mainBundle().pathForResource("lvwenhancom", ofType: "cer")!)!
network.addSSLPinning(LocalCertData: certData) { () -> Void in
    print("SSL 证书错误,遭受中间人攻击!")
}
network.fire()
return;

得到如下结果:

Image

接下来把网址改成 https://www.baidu.com/,运行,查看结果:

Image

搞定!

写在后面的话

本文中我只检测了经过第三方签名的受信任的 SSL 证书的检验结果,并没有测试自签名证书,希望有人测试之后把结果告诉我 :) 在文章下面评论或者上 Github 提 issue 都行~

《自己动手写一个 iOS 网络请求库》系列文章可能真的结束了,感谢你的阅读!

WRITTEN BY

avatar
2015.10.6   /   热度:11396   /   分类: iOS & Swift

评论:

GTMYang
2016-12-12 15:06
很厉害!受益匪浅
23号聪明莉
2016-11-01 21:21
控制台输出中文为Unicode字符 可否解决?
ccwangzh
2017-05-22 20:12
@23号聪明莉:使用JSON序列化后就可以啦
感谢
2016-01-09 15:22
受益匪浅
lifefarmer
2015-12-24 14:09
有没有使用Alamofire可以使用自己的证书的例子,Google了很多,都好像不能用
JohnLui
2015-12-24 14:35
@lifefarmer:欢迎使用 Pitaya~
lee
2015-10-16 10:34
收获很大谢谢分享
感谢信
2015-10-08 15:50
十分感谢,受益匪浅啊
游客1
2015-10-08 09:55
你的文章写的很好。
你能否出一篇,使用 摘要认证(Authorization)的相关文章,我自己摸索了一段时间,未能解决,请赐教。
JohnLui
2015-10-08 15:37
@游客1:我搜了一下,苹果官方有文档:https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/URLLoadingSystem/Articles/AuthenticationChallenges.html 这个关键字 NSURLAuthenticationMethodHTTPDigest

发表评论:

© 2011-2017 岁寒  |  Powered by Emlog