在咱们开发iOS运用的时分,许多时分,咱们都需求将灵敏数据(password, accessToken, secretKey等)存储到本地。对于初级程序员来讲,首先映入脑际的可能是运用UserDefaults。然而,众所周知,运用UserDefaults来存储灵敏信息简直是low的不能再low的主见了。由于咱们一般存储到UserDefaults中的数据都是未经过编码处理的,这样是十分不安全的。

为了能安全的在本地存储灵敏信息,咱们应当运用苹果供给的KeyChain服务。这个framework现已适当老了,所以,咱们在后面阅读的时分,会觉得它供给的API并不像当下的framework那么快捷。

在本文中,将为你展示如何创立一个通用的一同适用于iOS、MacOS的keyChain辅助类,对数据进行增修改查操作。开始吧!!!

保存数据到KeyChain

final class KeyChainHelper {
    static let standard = KeyChainHelper()
    private init(){}
}

咱们有必要巧妙运用SecItemAdd(_:_:)办法,这个办法会接纳一个CFDictionary类型的query目标。

这个主见是为了创立一个query目标,这个目标包含了咱们想要存储最主要的数据键值对。然后,将query目标传入SecItemAdd(_:_:)办法中来执行保存操作。

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)")
    }
}

回看上述代码片段,query目标由4个键值对组成:

  • kSecValueData: 这个键代表着数据现已被存储到了keyChain中
  • kSecClass: 这个键代表着数据现已被存储到了keyChain中。咱们将它的值设为了kSecClassGenericPassword,这代表着咱们所保存的数据是一个通用的暗码项
  • kSecAttrServicekSecAttrAccount: 当kSecClass被设置为kSecClassGenericPassword的时分,kSecAttrServicekSecAttrAccount这两个键是有必要要有的。这两个键所对应的值将作为所保存数据的要害key,换句话说,咱们将运用他们从keyChain中读取所保存的值。

对于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目标的kSecAttrServiceandkSecAttrAccount的值。在这之前,咱们需求为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"