我正在寻找在iOS钥匙串(keychain)中存储/加载accessToken和refreshToken的简单方法。
到目前为止我已经做到了:
enum Key: String {
case accessToken = "some.keys.accessToken"
case refreshToken = "some.keys.refreshToken"
fileprivate var tag: Data {
rawValue.data(using: .utf8)!
}
}
enum KeychainError: Error {
case storeFailed
case loadFailed
}
func store(key: Key, value: String) throws {
let addQuery: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: key.tag,
kSecValueRef as String: value
]
let status = SecItemAdd(addQuery as CFDictionary, nil)
guard status == errSecSuccess else {
print("Store key: '\(key.rawValue)' in Keychain failed with status: \(status.description)")
throw KeychainError.storeFailed
}
}
func load(key: Key) throws -> String? {
let getQuery: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: key.tag,
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
kSecReturnRef as String: true
]
var item: CFTypeRef?
let status = SecItemCopyMatching(getQuery as CFDictionary, &item)
guard status == errSecSuccess else {
print("Load key: '\(key.rawValue)' in Keychain failed with status: \(status.description)")
throw KeychainError.loadFailed
}
return item as? String
}
但是失败并显示消息:
运行store
时:
Store key: 'some.keys.accessToken' in Keychain failed with status: -50
运行加载
时:
Load key: 'some.keys.accessToken' in Keychain failed with status: -25300
我在这里做错了什么?
最佳答案
根据Apple的建议,您应该使用kSecClassGenericPassword
类来安全地存储任意数据,即 token 。要正确执行此操作,您需要在 kSecAttrAccount
键下存储一个 String
标识符,并在下存储一个安全值的 Data
表示形式kSecValueData
键。您可以通过执行以下操作轻松将字符串值转换为 Data
(假设 token 包含 UTF8 数据)。
tokenString.data(using: .utf8)
// or
Data(tokenString.utf8)
首先有一些不错的东西。
/// Errors that can be thrown when the Keychain is queried.
enum KeychainError: LocalizedError {
/// The requested item was not found in the Keychain.
case itemNotFound
/// Attempted to save an item that already exists.
/// Update the item instead.
case duplicateItem
/// The operation resulted in an unexpected status.
case unexpectedStatus(OSStatus)
}
/// A service that can be used to group the tokens
/// as the kSecAttrAccount should be unique.
let service = "com.bundle.stuff.token-service"
将 token 插入钥匙串(keychain)。
func insertToken(_ token: Data, identifier: String, service: String = service) throws {
let attributes = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: identifier,
kSecValueData: token
] as CFDictionary
let status = SecItemAdd(attributes, nil)
guard status == errSecSuccess else {
if status == errSecDuplicateItem {
throw KeychainError.duplicateItem
}
throw KeychainError.unexpectedStatus(status)
}
}
检索将按如下方式完成。
func getToken(identifier: String, service: String = service) throws -> String {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: identifier,
kSecMatchLimit: kSecMatchLimitOne,
kSecReturnData: true
] as CFDictionary
var result: AnyObject?
let status = SecItemCopyMatching(query, &result)
guard status == errSecSuccess else {
if status == errSecItemNotFound {
// Technically could make the return optional and return nil here
// depending on how you like this to be taken care of
throw KeychainError.itemNotFound
}
throw KeychainError.unexpectedStatus(status)
}
// Lots of bang operators here, due to the nature of Keychain functionality.
// You could work with more guards/if let or others.
return String(data: result as! Data, encoding: .utf8)!
}
请注意,如前所述,通用密码具有某些规范,我认为最重要的规范是 kSecAttrAccount
标志对于您存储的每个 token 必须是唯一的。您不能存储同一标识符的访问 token A 和访问 token B。这将导致触发 .duplicateItem
错误。
我还想指出 OSStatus website对于获取有关错误代码的更多信息非常有用。除了网站之外,还有 SecCopyErrorMessageString(OSStatus, UnsafeMutableRawPointer?)
函数可以获取有关错误代码的更多信息。
现在这从技术上回答了你的问题,但下面我添加了一些更好的东西。 Update 会更新现有项目的 token 值,请在调用 update! 之前确保该项目存在。 Upsert 在 token 尚不存在时插入 token ,如果存在,则会更新 token 值。删除将从钥匙串(keychain)中删除 token 值。
func updateToken(_ token: Data, identifier: String, service: String = service) throws {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: identifier
] as CFDictionary
let attributes = [
kSecValueData: token
] as CFDictionary
let status = SecItemUpdate(query, attributes)
guard status == errSecSuccess else {
if status == errSecItemNotFound {
throw KeychainError.itemNotFound
}
throw KeychainError.unexpectedStatus(status)
}
}
func upsertToken(_ token: Data, identifier: String, service: String = service) throws {
do {
_ = try getToken(identifier: identifier, service: service)
try updateToken(token, identifier: identifier, service: service)
} catch KeychainError.itemNotFound {
try insertToken(token, identifier: identifier, service: service)
}
}
func deleteToken(identifier: String, service: String = service) throws {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: identifier
] as CFDictionary
let status = SecItemDelete(query)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.unexpectedStatus(status)
}
}
关于ios - 将 accessToken 存储在 iOS 钥匙串(keychain)中,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68209016/