闽公网安备 35020302035485号
// 天真版本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.监控异常使用情况