在iOS开发中,敏感信息的本地存储一直是个让人头疼的问题——密码、accessToken、secretKey,随便一个泄露都可能引发安全事故。很多初级开发者下意识会选择UserDefaults,但说实话,用UserDefaults存敏感数据,几乎等于把密码写在便利贴上贴在屏幕边。数据未经编码直接暴露,安全隐患不言而喻。
真正靠谱的做法,是使用苹果提供的KeyChain服务。这个框架虽然有些年头了,API风格也不像当下的框架那么简洁顺手,但它的安全性是经过长期验证的。掌握iOS Keychain安全存储技巧,是每个Swift开发者必备的能力。
本文就来手把手教你创建一个同时适用于iOS和macOS的KeyChain辅助类,涵盖增删改查全套操作。准备好了吗?直接开干!
保存数据到KeyChain
先搭建一个单例辅助类:
final class KeyChainHelper {
static let standard = KeyChainHelper()
private init(){}
}
核心保存操作依赖SecItemAdd(_:_:)方法,它接收一个CFDictionary类型的query对象。思路很简单:构建一个包含待存储数据键值对的query,然后交给SecItemAdd执行。
func sa ve(_ data: Data, service: String, account: String) {
// Create query
let query = [
kSecValueData: data,
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: account,
] as CFDictionary
// Add data in query to keychain
let status = SecItemAdd(query, nil)
if status != errSecSuccess {
// Print out the error
print("Error: (status)")
}
}
这个query对象由四个关键键值对组成:
kSecValueData:代表实际要存储的数据kSecClass:这里设为kSecClassGenericPassword,表示存储的是一个通用密码项kSecAttrService和kSecAttrAccount:当kSecClass为通用密码项时,这两个键是必填的。它们组合起来相当于数据的“主键”,后续读取时也要用同样的组合来查找。推荐直接用字符串,比如存Facebook的accessToken时,可以把service设为"access-token",account设为"facebook"。
创建完query后,调用SecItemAdd。返回的OSStatus如果等于errSecSuccess,说明保存成功。使用方法如下:
let accessToken = "dummy-access-token" let data = Data(accessToken.utf8) KeychainHelper.standard.sa ve(data, service: "access-token", account: "facebook")
注意:KeyChain不能在playground中运行,所以上述代码要写在Controller里才行。
更新KeyChain中已有的数据
现在试试用相同的service和account存另一条token:
let accessToken = "another-dummy-access-token" let data = Data(accessToken.utf8) KeychainHelper.standard.sa ve(data, service: "access-token", account: "facebook")
你会遇到一个报错Error: -25299,对应的常量是errSecDuplicateItem。因为相同的key组合已经存在,所以直接添加会失败。解决办法很简单:检测到这个错误码后,改用SecItemUpdate(_:_:)来更新现有条目。我们改造一下sa ve方法:
func sa ve(_ data: Data, service: String, account: String) {
// ... ...
// ... ...
if status == errSecDuplicateItem {
// Item already exist, thus update it.
let query = [
kSecAttrService: service,
kSecAttrAccount: account,
kSecClass: kSecClassGenericPassword,
] as CFDictionary
let attributesToUpdate = [kSecValueData: data] as CFDictionary
// Update existing item
SecItemUpdate(query, attributesToUpdate)
}
}
更新操作和保存类似:先构建一个包含service和account的query,再另建一个只包含kSecValueData的字典,传给SecItemUpdate。这样一来,sa ve方法就同时具备了新增和更新的能力,既存则改,未存则加。这是实现KeyChain增删改查的基础逻辑。
从KeyChain中读取数据
读取的思路和保存几乎对称:同样先构建query,然后调用对应的读取方法:
func read(service: String, account: String) -> Data? {
let query = [
kSecAttrService: service,
kSecAttrAccount: account,
kSecClass: kSecClassGenericPassword,
kSecReturnData: true
] as CFDictionary
var result: AnyObject?
SecItemCopyMatching(query, &result)
return (result as? Data)
}
注意这里新增了一个kSecReturnData键,值为true,意思是我们希望KeyChain把匹配项的数据返回回来。然后通过SecItemCopyMatching方法,将结果通过引用赋给result。虽然这个方法也会返回一个OSStatus,但为简洁起见,这里省略了错误处理——如果读取失败,直接返回nil。
用法如下:
let data = KeychainHelper.standard.read(service: "access-token", account: "facebook")! let accessToken = String(data: data, encoding: .utf8)! print(accessToken)
从KeyChain中删除数据
没有删除功能的工具类是不完整的。删除比保存和读取更简单:
func delete(service: String, account: String) {
let query = [
kSecAttrService: service,
kSecAttrAccount: account,
kSecClass: kSecClassGenericPassword,
] as CFDictionary
// Delete item from keychain
SecItemDelete(query)
}
看到这段代码你应该很眼熟了——还是那个熟悉的query配方,然后直接调用SecItemDelete。整个流程非常自解释。通过这个KeyChain辅助类,你可以轻松管理iOS敏感数据存储。
创建一个通用的KeyChainHelper类
上面实现的几个方法只支持Data类型,实际开发中我们经常需要存储对象。如果能直接保存Codable类型就更方便了。下面是升级后的通用版本,让KeyChain支持任意Codable类型:
存储
func sa ve(_ item: T, service: String, account: String) where T : Codable { do { let data = try JSONEncoder().encode(item) sa ve(data, service: service, account: account) } catch { assertionFailure("Fail to encode item for keychain: (error)") } }
读取
func read(service: String, account: String, type: T.Type) -> T? where T : Codable { guard let data = read(service: service, account: account) else { return nil } do { let item = try JSONDecoder().decode(type, from: data) return item } catch { assertionFailure("Fail to decode item for keychain: (error)") return nil } }
实际使用
struct Auth: Codable {
let accessToken: String
let refreshToken: String
}
let auth = Auth(accessToken: "dummy-access-token",
refreshToken: "dummy-refresh-token")
let account = "domain.com"
let service = "token"
// 保存
KeychainHelper.standard.sa ve(auth, service: service, account: account)
// 读取
let result = KeychainHelper.standard.read(service: service,
account: account,
type: Auth.self)!
print(result.accessToken) // Output: "dummy-access-token"
print(result.refreshToken) // Output: "dummy-refresh-token"
这样一来,任何满足Codable的结构体或类都可以直接存入KeyChain,而无需手动做Data转换。安全存储敏感信息,这一套组合拳算是彻底拿下了。如果你正在寻找Swift KeyChain教程或iOS Keychain安全存储的最佳实践,这个方案能直接应用到项目中。
