URLSession 系列第四篇:彻底防止中间人攻击,确保网络安全!

1,384 阅读6分钟

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

在前几篇文章中,我们探讨了 URLSession 的基本使用和异步编程方法。本篇文章将重点介绍一个大家比较关心,而且面试中经常问到的问题,如何使用 URLSession 防止中间人攻击(Man-in-the-Middle Attack,简称 MITM),确保网络通信的安全性。

在阅读这篇文章之前,建议你先阅读我前几篇关于 URLSession 的文章,这样你会更容易理解这篇文章的内容。

URLSession 系列第一篇:iOS 开发者必须掌握的 URLSession 使用指南

什么是中间人攻击?

中间人攻击是一种常见的网络攻击方式,攻击者通过拦截和篡改通信双方之间的数据,获取敏感信息或注入恶意代码。为了防止中间人攻击,我们需要确保客户端与服务器之间的通信是安全的。

举个例子,我们常用的 Charles 抓包就算是一种中间人,抓到某个请求后,我们可以修改一些请求参数,重新发送给服务端(比如充值 1 元,改成 100 元),这种就是一个经典的中间人攻击。

使用 URLSession 防止中间人攻击

回到正题,要防止中间人攻击,关键是确保通信过程中使用的证书是可信的,且未被篡改。我们可以通过以下几个步骤来实现这一目标:

  1. 启用 HTTPS:确保所有网络请求使用 HTTPS 协议,而不是 HTTP。

  2. 验证服务器证书:在客户端手动验证服务器证书,确保其可信。

  3. 使用证书固定(Certificate Pinning):将服务器的公钥或证书固定在客户端,防止攻击者使用伪造的证书进行中间人攻击。

接下来我们分别说说这三个步骤。

启用 HTTPS

启用 HTTPS 是防止中间人攻击的基础步骤。HTTPS 使用 TLS(传输层安全协议)加密通信数据,确保数据在传输过程中不会被窃取或篡改,这里就不过多介绍了。

URLSession 中,默认情况下会使用 HTTPS 协议,只需确保你的服务器支持 HTTPS 并正确配置了 SSL/TLS 证书。

验证服务器证书

我们可以通过 URLSession 的委托方法 urlSession(_:didReceive:completionHandler:) 来手动验证服务器证书,让我们稍微改改 NetworkManager 这个类(这个类是前几篇文章中写的,可以先去补补之前的内容)。

另外我们还需要创建一个新的类 URLSessionDelegate 来处理代理方法,代码如下:

// 创建一个处理 URLSessionDelegate 的类
class DelegateHandler: NSObject, URLSessionDelegate {
     func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
         if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
            let serverTrust = challenge.protectionSpace.serverTrust {
             let credential = URLCredential(trust: serverTrust)
             completionHandler(.useCredential, credential)
         } else {
             completionHandler(.performDefaultHandling, nil)
         }
     }
}

class NetworkManager {
    // 创建一个单例 NetworkManager 对象
    static let shared = NetworkManager()
    
    // 一个共享的 URLSession 对象
    private let session: URLSession

    // 防止其他对象创建 NetworkManager 对象,所以将 init() 方法私有化
    private init() {
        let configuration = URLSessionConfiguration.default
        // 设置代理为 DelegateHandler 对象
        self.session = URLSession(configuration: configuration, delegate: DelegateHandler(), delegateQueue: nil)
    }
}

在这个代码中,urlSession(_:didReceive:completionHandler:) 方法在接收到服务器的证书认证时被调用。此时会检查服务器的证书,并自动信任它。

这里信任了所有有效的证书,所以还不能防止中间人攻击。

使用证书固定(Certificate Pinning)

为了更进一步确保安全性,我们可以使用证书固定(Certificate Pinning)。证书固定是一种将服务器的公钥或证书直接嵌入到客户端的技术,这样即使攻击者使用伪造的证书进行中间人攻击,客户端也能检测到。

简单来说就是把服务器的证书放到客户端,当有请求过来校验证书时,我们用本地存储的证书来和服务端的证书做对比,如果相同,则信任,否则拒绝。

获取服务器证书

首先,你需要获取服务器的证书并将其嵌入到客户端应用中。可以通过浏览器或命令行工具获取证书。

使用 openssl 命令获取证书,我们这里就以 jsonplaceholder.typicode.com 域名为例,在终端执行以下命令:

echo | openssl s_client -connect jsonplaceholder.typicode.com:443 -servername jsonplaceholder.typicode.com 2>/dev/null | openssl x509 > server.cer

稍微分解一下这个命令:

  1. echo |:这是为了向 openssl s_client 发送一个空输入,以便它立即返回。

  2. openssl s_client -connect jsonplaceholder.typicode.com:443 -servername jsonplaceholder.typicode.com:这是与指定的服务器建立 SSL 连接。

  3. 2>/dev/null:这是为了忽略错误信息,只获取我们需要的输出。

  4. openssl x509:这是用来解析和显示证书的命令。

  5. server.cer:这是将证书输出重定向到 server.cer 文件中。

最终将输出的证书内容保存到一个 server.cer 文件中。

也可以通过这个命令来查看证书的内容:

openssl s_client -connect yourserver.com:443 -showcerts

也可以直接用文本编辑器打开 server.cer 文件查看证书内容:

将证书添加到项目中

将获取的证书文件 server.cer 添加到 Xcode 项目中。

实现证书固定

最关键的步骤来了,在 DelegateHandler 中实现证书校验的逻辑:

// 创建一个处理 URLSessionDelegate 的类
class DelegateHandler: NSObject, URLSessionDelegate {
    // 检查服务器证书是否是指定的证书
    private func isServerTrusted(serverTrust: SecTrust) -> Bool {
        guard let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
            return false
        }
        let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data
        let certPath = Bundle.main.path(forResource: "server", ofType: "cer")!
        let contents = try! String(contentsOfFile: certPath)
            .replacingOccurrences(of: "-----BEGIN CERTIFICATE-----", with: "")
            .replacingOccurrences(of: "-----END CERTIFICATE-----", with: "")
            .replacingOccurrences(of: "\n", with: "")
        
        let localCertificateData = Data(base64Encoded: contents)!
        return serverCertificateData == localCertificateData
    }
    
    // 实现 URLSessionDelegate 的方法
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
           let serverTrust = challenge.protectionSpace.serverTrust {
            if isServerTrusted(serverTrust: serverTrust) {
                let credential = URLCredential(trust: serverTrust)
                completionHandler(.useCredential, credential)
            } else {
                completionHandler(.cancelAuthenticationChallenge, nil)
            }
        } else {
            completionHandler(.performDefaultHandling, nil)
        }
    }
}

解释一下代码,在这个示例中,定义了一个名为 DelegateHandler 的类,它实现了 URLSessionDelegate 协议,用于处理与服务器进行安全通信时的证书验证。

isServerTrusted(serverTrust:) 方法中,我们首先获取服务器证书,然后将其与本地存储的证书进行比较。如果两者相同,则信任服务器证书,否则拒绝连接。

urlSession(_:didReceive:completionHandler:) 方法中,我们检查服务器证书是否可信,如果是,则使用服务器证书创建一个 URLCredential 对象,否则取消连接。

这样写完之后,我们就实现了防止中间人攻击的能力,可以再试试,如果开启了 Charles 抓包,请求也是无法正常返回的。

总结

在这篇文章中,我们介绍了如何使用 URLSession 防止中间人攻击。通过启用 HTTPS、验证服务器证书以及使用证书固定(Certificate Pinning),我们可以显著提高网络通信的安全性,防止中间人攻击。

希望这篇文章对你有所帮助,也希望能够帮你更进一步理解中间人攻击相关的知识,在实际项目中应用这些技术能保障网络通信的安全。下一篇文章将继续探讨 URLSession 的其他高级用法,敬请期待!

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!