You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

iOS中如何实现SSL Pinning?求最优方案、获取方式及示例代码

iOS SSL Pinning:证书 vs 公钥,该怎么选?

我来帮你理清这两种实现方式的区别,以及实际开发中的选择逻辑,再附上获取证书/公钥的方法和代码示例:

一、哪种实现方法更优?

这得看你的业务场景,两者各有优劣:

  • 证书绑定(Certificate Pinning)
    • 优点:验证逻辑最严格,能彻底防范证书伪造类的中间人攻击,因为它会校验服务器返回的整个证书文件和你本地打包的完全一致。
    • 缺点:服务器证书一旦更新(比如续期、更换证书),你的APP必须同步更新本地证书并重新发布,否则用户会无法访问服务。维护成本较高,适合证书更新频率极低、安全要求极高的场景。
  • 公钥绑定(Public Key Pinning)
    • 优点:灵活性更强!很多时候服务器续期证书时会沿用同一个公钥,这种情况下你的APP不需要任何更新就能正常工作,大大降低了维护成本。而且Apple在相关文档里也更推荐这种方式,因为它平衡了安全性和兼容性。
    • 缺点:如果攻击者真的拿到了服务器的私钥(这种情况本身属于严重安全事故),公钥绑定就无法防范了,但这种情况发生的概率极低。

总结:绝大多数日常开发场景下,优先选公钥绑定;如果你的业务对安全要求极高,且能完全控制证书更新节奏(比如内部企业APP),可以考虑证书绑定。

二、如何获取Certificate与Public Key?

获取证书(.cer文件)

有两种简单方法:

  1. Safari可视化操作:打开你的服务器域名,点击地址栏的锁图标 → 「显示证书」 → 把证书图标拖到桌面,就能得到一个.cer格式的文件。
  2. 命令行方式
    openssl s_client -connect yourdomain.com:443 < /dev/null | openssl x509 -outform der > certificate.cer
    
    yourdomain.com换成你的实际域名就行。

获取Public Key(Base64字符串)

  1. 先从刚才的证书文件提取PEM格式的公钥:
    openssl x509 -in certificate.cer -inform der -pubkey -noout > public_key.pem
    
  2. 打开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

火山引擎 最新活动