// 天真版本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 试过,简直不要太简单,几分钟就能把所有字符串都导出来...好了,吐槽完自己的黑历史,来看看正经解决方案。
API_KEY = sk_test_abcdefghijklmnopqrstuvwxyz123456第二步:在 Configuration 中引用它
<key>APIKey</key> <string>$(API_KEY)</string>
if let apiKey = Bundle.main.infoDictionary?["APIKey"] as? String { // 开始愉快地调用API }这种方法最大的好处就是简单,连我这种懒人都能轻松实现。不过坦白说,这招只能防君子不防小人 —— 虽然源代码中看不到密钥,但编译后的应用里,Info.plist中的密钥是明文保存的,用工具很容易就能提取出来。
{ "MyServiceX": "sk_test_abcdefghijklmnopqrstuvwxyz123456", "MyServiceY": "其他密钥" }
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) } } } } }这个方案的安全级别比方案一要高,因为资源不会一开始就打包到应用里,而是按需下载。但说实话,如果有人真的很想破解,这种方法也挡不住,只是增加了一点难度。
// 客户端代码 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,这绝对是值得的投入。
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账号,如果做国际化可能会有些问题。
// 使用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) ?? "" } }这个方法需要额外设置一个构建脚本,我第一次配置时搞了半天才成功。原理是在构建时用混淆工具自动生成上面这段代码,而不是手写。老实说,这种方法也不是绝对安全,只是增加了破解的难度。但对于黑客来说,如果真想搞定你,花点时间还是能破解的。我一个朋友就是安全专家,他告诉我这种混淆对专业人士其实没太大用,但至少能阻挡住那些随便用工具提取字符串的人。
4.独立开发者/小团队的正式产品: 方案四是个不错的折衷,没有后端也能做到相对安全
4.监控异常使用情况