iOS-Datenpersistenz – KeyChain

Wenn wir iOS-Anwendungen entwickeln, müssen wir oft sensible Daten (Passwort, AccessToken, SecretKey usw.) lokal speichern. Für Junior-Programmierer ist das erste, was einem in den Sinn kommt, use UserDefaults. Wie wir alle wissen, UserDefaultsist die Verwendung zum Speichern vertraulicher Informationen jedoch nur eine Idee auf niedriger Ebene. Da die Daten, die wir normalerweise speichern, UserDefaultsunverschlüsselt sind, ist dies sehr unsicher.

Um sensible Informationen sicher lokal zu speichern, sollten wir KeyChaindie von Apple bereitgestellten Dienste nutzen. Dieses Framework ist ziemlich alt, und wenn wir es später lesen, werden wir das Gefühl haben, dass die bereitgestellte API nicht so schnell ist wie das aktuelle Framework.

In diesem Artikel zeige ich Ihnen, wie Sie eine Allzweck-KeyChain-Hilfsklasse erstellen, die sowohl für iOS als auch für MacOS geeignet ist, um Operationen zum Hinzufügen, Löschen, Ändern und Abfragen von Daten durchzuführen. Lasst uns beginnen! ! !

Daten im Schlüsselbund speichern

final class KeyChainHelper {
    static let standard = KeyChainHelper()
    private init(){}
}
复制代码

Wir müssen SecItemAdd(_:_:)die Methode geschickt verwenden, diese Methode erhält eine CFDictionaryArt Abfrageobjekt.

Die Idee ist, ein Abfrageobjekt zu erstellen, das Schlüssel-Wert-Paare der wichtigsten Daten enthält, die wir speichern möchten. Übergeben Sie dann das Abfrageobjekt an SecItemAdd(_:_:)die Methode, um den Speichervorgang auszuführen.

func save(_ 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)")
    }
}
复制代码

Rückblickend auf das obige Code-Snippet besteht das Abfrageobjekt aus 4 Schlüssel-Wert-Paaren:

  • kSecValueData: Dieser Schlüssel stellt dar, dass die Daten im Schlüsselbund gespeichert wurden
  • kSecClass: Dieser Schlüssel stellt dar, dass die Daten im Schlüsselbund gespeichert wurden. Wir setzen seinen Wert auf kSecClassGenericPassword, was bedeutet, dass die von uns gespeicherten Daten ein allgemeines Passwortelement sind
  • kSecAttrServiceund kSecAttrAccount: Bei kSecClassEinstellung auf kSecClassGenericPasswordsind die Tasten kSecAttrServiceund kSecAttrAccounterforderlich. Der Wert, der diesen beiden Schlüsseln entspricht, wird als Schlüsselschlüssel der gespeicherten Daten verwendet, mit anderen Worten, wir werden sie verwenden, um den gespeicherten Wert aus dem Schlüsselbund zu lesen.

对于kSecAttrServicekSecAttrAccount所对应的值的定义并没有什么难的。推荐使用字符串。例如:如果我们想存储Facebook的accesToken,我们需要将kSecAttrService设置成”access-token“,将kSecAttrAccount设置成”facebook“

创建完query对象之后,我们可以调用SecItemAdd(_:_:)方法来保存数据到keyChain。SecItemAdd(_:_:)方法会返回一个OSStatus来代表存储状态。如果我们得到的是errSecSuccess状态,则意味着数据已经被成功保存到keyChain中

下面是save(_:service:account:)方法的使用

let accessToken = "dummy-access-token"
let data = Data(accessToken.utf8)
KeychainHelper.standard.save(data, service: "access-token", account: "facebook")
复制代码

keyChain不能在playground中使用,所以,上述代码必须写在Controller中。

更新KeyChain中已有的数据

现在我们有了save(_:service:account:)方法,让我们用相同的kSecAttrServicekSecAttrAccount所对应的值来存储其他token

let accessToken = "another-dummy-access-token"
let data = Data(accessToken.utf8)
KeychainHelper.standard.save(data, service: "access-token", account: "facebook")
复制代码

这时候,我们就无法将accessToken保存到keyChain中了。同时,我们会得到一个Error: -25299的报错。该错误码代表的是存储失败。因为我们所使用的keys已经存在于keyChain当中了。

为了解决这个问题,我们需要检查这个错误码(相当于errSecDuplicateItem),然后使用SecItemUpdate(_:_:)方法来更新keyChain。一起看看并更新我们前述的save(_:service:account:)方法吧:

func save(_ 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)
    }
}
复制代码

跟保存操作相似的是,我们需要先创建一个query对象,这个对象包含kSecAttrServicekSecAttrAccount。但是这次,我们将会创建另外一个包含kSecValueData的字典,并将它传给SecItemUpdate(_:_:)方法。

这样的话,我们就可以让save(_:service:account:)方法来更新keyChain中已有的数据了。

从KeyChain中读取数据

从keyChain中读取数据的方式和保存的方式非常相似。我们首先要做的是创建一个query对象,然后调用一个keyChain方法:

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)
}
复制代码

跟之前一样,我们需要设置query对象的kSecAttrService and kSecAttrAccount的值。在这之前,我们需要为query对象添加一个新的键kSecReturnData,其值为true,代表的是我们希望query返回对应项的数据。

之后,我们将利用 SecItemCopyMatching(_:_:) 方法并通过引用传入 AnyObject 类型的result对象。SecItemCopyMatching(_:_:)方法同样返回一个OSStatus类型的值,代表读取操作状态。但是如果读取失败了,这里我们不做任何校验,并返回nil

让keyChain支持读取的操作就这么多了,看一下他是怎么工作的吧

let data = KeychainHelper.standard.read(service: "access-token", account: "facebook")!
let accessToken = String(data: data, encoding: .utf8)!
print(accessToken)
复制代码

从KeyChain中删除数据

如果没有删除操作,我们的KeyChainHelper类并不算完成。一起看看下面的代码片段吧

func delete(service: String, account: String) {
    
    let query = [
        kSecAttrService: service,
        kSecAttrAccount: account,
        kSecClass: kSecClassGenericPassword,
        ] as CFDictionary
    
    // Delete item from keychain
    SecItemDelete(query)
}
复制代码

如果你全程都在看的话,上述代码可能对你来说非常熟悉,那是相当的”自解释“了,需要注意的是,这里我们使用了SecItemDelete(_:)方法来删除KeyChain中的数据了。

创建一个通用的KeyChainHelper 类

存储

func save<T>(_ item: T, service: String, account: String) where T : Codable {
    
    do {
        // Encode as JSON data and save in keychain
        let data = try JSONEncoder().encode(item)
        save(data, service: service, account: account)
        
    } catch {
        assertionFailure("Fail to encode item for keychain: (error)")
    }
}
复制代码

读取

func read<T>(service: String, account: String, type: T.Type) -> T? where T : Codable {
    
    // Read item data from keychain
    guard let data = read(service: service, account: account) else {
        return nil
    }
    
    // Decode JSON data to object
    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
}

// Create an object to save
let auth = Auth(accessToken: "dummy-access-token",
                 refreshToken: "dummy-refresh-token")

let account = "domain.com"
let service = "token"

// Save `auth` to keychain
KeychainHelper.standard.save(auth, service: service, account: account)

// Read `auth` from keychain
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"
复制代码

Supongo que te gusta

Origin juejin.im/post/7195486949526732857
Recomendado
Clasificación