iOS中如何实现SSL Pinning?求最优方案、获取方式及示例代码
iOS SSL Pinning:证书 vs 公钥,该怎么选?
我来帮你理清这两种实现方式的区别,以及实际开发中的选择逻辑,再附上获取证书/公钥的方法和代码示例:
一、哪种实现方法更优?
这得看你的业务场景,两者各有优劣:
- 证书绑定(Certificate Pinning):
- 优点:验证逻辑最严格,能彻底防范证书伪造类的中间人攻击,因为它会校验服务器返回的整个证书文件和你本地打包的完全一致。
- 缺点:服务器证书一旦更新(比如续期、更换证书),你的APP必须同步更新本地证书并重新发布,否则用户会无法访问服务。维护成本较高,适合证书更新频率极低、安全要求极高的场景。
- 公钥绑定(Public Key Pinning):
- 优点:灵活性更强!很多时候服务器续期证书时会沿用同一个公钥,这种情况下你的APP不需要任何更新就能正常工作,大大降低了维护成本。而且Apple在相关文档里也更推荐这种方式,因为它平衡了安全性和兼容性。
- 缺点:如果攻击者真的拿到了服务器的私钥(这种情况本身属于严重安全事故),公钥绑定就无法防范了,但这种情况发生的概率极低。
总结:绝大多数日常开发场景下,优先选公钥绑定;如果你的业务对安全要求极高,且能完全控制证书更新节奏(比如内部企业APP),可以考虑证书绑定。
二、如何获取Certificate与Public Key?
获取证书(.cer文件)
有两种简单方法:
- Safari可视化操作:打开你的服务器域名,点击地址栏的锁图标 → 「显示证书」 → 把证书图标拖到桌面,就能得到一个
.cer格式的文件。 - 命令行方式:
把openssl s_client -connect yourdomain.com:443 < /dev/null | openssl x509 -outform der > certificate.ceryourdomain.com换成你的实际域名就行。
获取Public Key(Base64字符串)
- 先从刚才的证书文件提取PEM格式的公钥:
openssl x509 -in certificate.cer -inform der -pubkey -noout > public_key.pem - 打开
public_key.pem文件,去掉开头的-----BEGIN PUBLIC KEY-----和结尾的-----END PUBLIC KEY-----,然后删掉所有换行符,剩下的就是你需要的Base64格式公钥字符串。
三、Swift示例代码
公钥绑定实现
import Foundation import Security class PublicKeyPinningSessionDelegate: NSObject, URLSessionDelegate { func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { // 1. 获取服务器信任对象 guard let serverTrust = challenge.protectionSpace.serverTrust else { completionHandler(.cancelAuthenticationChallenge, nil) return } // 2. 替换成你自己的公钥Base64字符串 let pinnedPublicKeyBase64 = "这里填你的公钥Base64字符串" // 3. 提取服务器证书的公钥数据 guard let serverPublicKey = SecTrustCopyPublicKey(serverTrust), var error: Unmanaged<CFError>? = nil, let serverPublicKeyData = SecKeyCopyExternalRepresentation(serverPublicKey, &error) as Data? else { completionHandler(.cancelAuthenticationChallenge, nil) return } // 4. 解析本地绑定的公钥数据 guard let pinnedPublicKeyData = Data(base64Encoded: pinnedPublicKeyBase64) else { completionHandler(.cancelAuthenticationChallenge, nil) return } // 5. 对比公钥,一致则通过验证 if serverPublicKeyData == pinnedPublicKeyData { let credential = URLCredential(trust: serverTrust) completionHandler(.useCredential, credential) } else { completionHandler(.cancelAuthenticationChallenge, nil) } } } // 使用方式 let session = URLSession(configuration: .default, delegate: PublicKeyPinningSessionDelegate(), delegateQueue: nil) let requestURL = URL(string: "https://yourdomain.com/api")! let task = session.dataTask(with: requestURL) { data, response, error in // 处理请求结果 } task.resume()
证书绑定实现
import Foundation import Security class CertificatePinningSessionDelegate: NSObject, URLSessionDelegate { func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { // 1. 获取服务器信任对象 guard let serverTrust = challenge.protectionSpace.serverTrust else { completionHandler(.cancelAuthenticationChallenge, nil) return } // 2. 加载本地打包的证书(把certificate.cer拖进项目,勾选对应Target) guard let certURL = Bundle.main.url(forResource: "certificate", withExtension: "cer"), let certData = try? Data(contentsOf: certURL), let localCertificate = SecCertificateCreateWithData(nil, certData as CFData) else { completionHandler(.cancelAuthenticationChallenge, nil) return } // 3. 设置信任策略:只信任本地证书 let sslPolicy = SecPolicyCreateSSL(true, challenge.protectionSpace.host as CFString) SecTrustSetPolicies(serverTrust, sslPolicy) SecTrustSetAnchorCertificates(serverTrust, [localCertificate] as CFArray) SecTrustSetAnchorCertificatesOnly(serverTrust, true) // 4. 验证服务器信任 var trustResult: SecTrustResultType = .invalid let evaluateStatus = SecTrustEvaluate(serverTrust, &trustResult) if evaluateStatus == errSecSuccess && (trustResult == .unspecified || trustResult == .proceed) { let credential = URLCredential(trust: serverTrust) completionHandler(.useCredential, credential) } else { completionHandler(.cancelAuthenticationChallenge, nil) } } } // 使用方式 let session = URLSession(configuration: .default, delegate: CertificatePinningSessionDelegate(), delegateQueue: nil) let requestURL = URL(string: "https://yourdomain.com/api")! let task = session.dataTask(with: requestURL) { data, response, error in // 处理请求结果 } task.resume()
内容的提问来源于stack exchange,提问作者Rohit Sisodia




