• 如何拯救你的API密钥:iOS中隐藏敏感信息的方案都有哪些?
  • 发布于 1个月前
  • 188 热度
    0 评论
前言
最近跟几个朋友聊天,发现大家最头疼的问题之一就是API密钥的安全性。说实话,我也是踩过无数坑才意识到,把API密钥直接放在代码里,简直就像是把家门钥匙挂在门外 —— 哪个小偷看了不笑啊?我有个朋友去年有个项目,天真的把一个付费 API 的密钥直接写在代码里,然后不小心把代码推到了 GitHub 上...结果一个月收到了一笔"天价"账单,都不好意思说出来是多少 从那以后,他就疯狂研究各种 API 密钥保护方案,今天就把这些血泪教训分享给大家。

如果你的 App 还在用硬编码的方式存储密钥,赶紧看下去!这可能是你今天读到的最有价值的一篇文章。

为什么 API 密钥需要保护?
在真正开始之前,咱们得明白为什么这玩意这么重要:
费用控制:不少 API 服务都是按量收费的,密钥泄露后被人刷爆,分分钟几万块没了(别问我怎么知道的...)
数据安全:密钥一旦泄露,你的用户数据可能就被人偷走了,这可不是闹着玩的
服务稳定:别人用你的 API 配额,结果你自己的用户用不了服务,这不是搬起石头砸自己的脚嘛
品牌信任:数据泄露可是会上新闻的,这种负面影响你懂的...
前两天看到一个新闻,某知名 App 的 API 密钥泄露,导致他们的Google Maps账单在一周内暴增超过10万美元,产品经理差点没哭出来。我身边也有朋友因为API密钥泄露,AWS账单一夜之间暴涨几千块。这都是血淋淋的教训啊!

常见的错误做法
开始正题前,咱们先看看那些我曾经(嗯...就是我自己)犯过的错误:
// 天真版本1:直接硬编码
let apiKey = "sk_test_abcdefghijklmnopqrstuvwxyz123456"
// 堆代码 duidaima.com
// 天真版本2:以为放在类里面就安全了
class APIManager {
    static let shared = APIManager()
    private let apiKey = "sk_test_abcdefghijklmnopqrstuvwxyz123456"
}
还有更可爱的做法,我之前真的用过:
// 天真版本3:改个变量名,觉得黑客就找不到了
let x837dhg = "sk_test_abcdefghijklmnopqrstuvwxyz123456" // 这名字够随机了吧?嘿嘿
现在回想起来都有点脸红。事实是,任何直接硬编码在应用中的密钥,都可以被反编译出来。我用 Hopper 试过,简直不要太简单,几分钟就能把所有字符串都导出来...好了,吐槽完自己的黑历史,来看看正经解决方案。

方案一:使用配置文件(.xcconfig
这是我在个人项目中最常用的方案,实现起来特别简单:
1.首先,新建一个Secrets.xcconfig文件
2.把 API 密钥存在这个文件里
3.确保把它加到 .gitignore 中(可别忘了这步!)
4.构建时从配置文件中读取密钥
具体操作如下:
第一步:创建配置文件

然后写入密钥:
API_KEY = sk_test_abcdefghijklmnopqrstuvwxyz123456
第二步:在 Configuration 中引用它

第三步:在 Info.plist 中引用它
<key>APIKey</key>
<string>$(API_KEY)</string>

第四步:代码中这样用
if let apiKey = Bundle.main.infoDictionary?["APIKey"] as? String {
    // 开始愉快地调用API
}
这种方法最大的好处就是简单,连我这种懒人都能轻松实现。不过坦白说,这招只能防君子不防小人 —— 虽然源代码中看不到密钥,但编译后的应用里,Info.plist中的密钥是明文保存的,用工具很容易就能提取出来。

方案二:通过按需资源(On-Demand Resources)保护API密钥
这是我最近才开始使用的一个小技巧,利用了苹果的按需资源机制:
第一步:在 Xcode 项目设置中,找到"Resource Tags"标签,创建一个名为"APIKeys"的资源标签。
第二步:创建一个 JSON 文件,填入你的密钥:
{
  "MyServiceX": "sk_test_abcdefghijklmnopqrstuvwxyz123456",
  "MyServiceY": "其他密钥"
}

记得关联这个文件和刚才创建的标签。有意思的是,我在用这个方法时,第一次竟然把标签名写错了,结果app启动就崩溃,排查了好久才发现🤦‍♂️

第三步:运行时加载密钥
enum KeyConstants {
staticfunc loadAPIKeys() async throws {
    let request = NSBundleResourceRequest(tags: ["APIKeys"])
    try await request.beginAccessingResources()
    
    let url = Bundle.main.url(forResource: "Secrets", withExtension: "json")!
    let data = tryData(contentsOf: url)
    APIKeys.storage = tryJSONDecoder().decode([String: String].self, from: data)
    
    request.endAccessingResources()
  }

enum APIKeys {
    staticfileprivate(set) var storage = [String: String]()
    staticvar myServiceXKey: String { storage["MyServiceX"] ?? "" }
    staticvar myServiceYKey: String { storage["MyServiceY"] ?? "" }
  }
}
在 SwiftUI 应用中这么调用:
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .task {
                    do {
                        try await KeyConstants.loadAPIKeys()
                    } catch {
                        print(error.localizedDescription)
                    }
                }
        }
    }
}
这个方案的安全级别比方案一要高,因为资源不会一开始就打包到应用里,而是按需下载。但说实话,如果有人真的很想破解,这种方法也挡不住,只是增加了一点难度。

方案三:服务器端验证与密钥管理
这是我在公司项目中的标准配置,尤其是涉及到支付、金融的那些项目:
1.移动端完全不存储API密钥
2.App通过自己的后端服务器进行API调用
3.服务器保管密钥,验证请求,然后调用第三方服务
具体实现大概是这样:
// 客户端代码
struct APIService {
    func fetchData() async throws -> SomeData {
        // 请求打到自己的服务器,而不是直接去第三方
        let url = URL(string: "https://your-backend-server.com/api/proxy-request")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        // 堆代码 duidaima.com
        // 用自己的认证系统,而不是第三方的API密钥
        request.addValue("Bearer \(userToken)", forHTTPHeaderField: "Authorization")
        
        let (data, _) = try await URLSession.shared.data(for: request)
        returntryJSONDecoder().decode(SomeData.self, from: data)
    }
}
服务器收到请求后,会验证用户身份,然后才使用保存在安全位置的API密钥调用第三方服务。这种方案是我心目中的最佳实践,但说实话,对于小团队来说确实有些重(我自己的side project就舍不得用这个方案,维护成本太高了)。它需要额外维护一个后端服务,增加了不少工作量和成本。不过对于严肃的商业应用,特别是金融类App,这绝对是值得的投入。

方案四:使用CloudKit分发密钥
如果你既想安全,又不想维护后端,可以试试这个我去年发现的方案 —— 利用CloudKit分发密钥:
import CloudKit
import KeychainAccess

class APIKeyManager {
    privatelet container = CKContainer.default()
    privatelet keychain = Keychain(service: "com.yourapp.service")

    func retrieveAPIKey() async {
        do {
            let publicDB = container.publicCloudDatabase
            let predicate = NSPredicate(value: true)
            let query = CKQuery(recordType: "APIKeys", predicate: predicate)
            
            let (records, _) = try await publicDB.records(matching: query)
            iflet record = records.first?.1,
               let encryptedKey = record["encryptedKey"] as? String {
                let apiKey = decryptKey(encryptedApiKey: encryptedKey)
                keychain["apiKey"] = apiKey
            }
        } catch {
            print("Failed to retrieve API key: \(error)")
        }
    }
    
    privatefunc decryptKey(encryptedApiKey: String) -> String {
        // 这里需要实现解密逻辑
        // 我通常用CryptoKit实现,不过代码有点长,这里就省略了
        return"decrypted_key"
    }
}
你需要先在CloudKit控制台创建一个记录类型来存储加密后的API密钥。App启动时,先从CloudKit拉取密钥,解密后存到Keychain里。这种方法挺巧妙的,我个人很喜欢,算是找到了一个不用自建后端又能相对安全的平衡点。缺点是需要用户有iCloud账号,如果做国际化可能会有些问题。

方案五:混淆技术保护密钥
如果你既不想搭建服务器,又对 CloudKit 有顾虑,还可以尝试代码混淆技术,iOS 里有个开源库叫 cocoapods-keys,其原理就是如此:
// 使用GYB工具在构建时生成混淆密钥
enum APIKeys {
    // 下面这部分每次构建都会自动生成,源代码里看不到真正的密钥
    staticlet obfuscatedKey: String = "\x68\x65\x6c\x6c\x6f"
    staticlet salt: [UInt8] = [0xae, 0xbf, 0x20, ...] // 每次构建不同
    
    staticvar apiKey: String {
        let obfuscatedBytes = [UInt8](obfuscatedKey.utf8)
        let decodedBytes = zip(obfuscatedBytes, salt).map { $0 ^ $1 }
        returnString(bytes: decodedBytes, encoding: .utf8) ?? ""
    }
}
这个方法需要额外设置一个构建脚本,我第一次配置时搞了半天才成功。原理是在构建时用混淆工具自动生成上面这段代码,而不是手写。老实说,这种方法也不是绝对安全,只是增加了破解的难度。但对于黑客来说,如果真想搞定你,花点时间还是能破解的。我一个朋友就是安全专家,他告诉我这种混淆对专业人士其实没太大用,但至少能阻挡住那些随便用工具提取字符串的人。

实际项目该怎么选?
经过这些年的实践,我逐渐总结出了不同场景下的最佳选择:
1.个人小项目/MVP阶段: 用方案一就够了,反正也没人黑你
2.中小型商业应用: 方案二或方案五比较平衡,安全性和实现难度都适中
3.金融/医疗/企业应用: 必须用方案三,安全不能有一丝妥协

4.独立开发者/小团队的正式产品: 方案四是个不错的折衷,没有后端也能做到相对安全


无论选哪种方案,以下这些通用措施必不可少:
1.给API设置调用限制
2.给密钥设置最小必要权限,别一股脑儿给admin权限
3.定期更换密钥

4.监控异常使用情况


总结
说了这么多,希望你已经找到了适合自己项目的方案。记住一点:安全从来不是一劳永逸的事情,而是需要持续投入和更新的过程。就像我去年信心满满地用了方案一,结果被朋友轻松破解一样,今天看起来安全的方案,明天可能就不再安全了。不过好在技术在进步,苹果也在不断完善安全框架,相信在不久的将来,会有更便捷、更安全的API密钥保护方案出现。
用户评论