iOS資料持久化——KeyChain
在我們開發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
,這代表著我們所儲存的資料是一個通用的密碼項
- kSecAttrService
和kSecAttrAccount
: 當kSecClass
被設定為kSecClassGenericPassword
的時候,kSecAttrService
和kSecAttrAccount
這兩個鍵是必須要有的。這兩個鍵所對應的值將作為所儲存資料的關鍵key,換句話說,我們將使用他們從keyChain中讀取所儲存的值。
對於kSecAttrService
和kSecAttrAccount
所對應的值的定義並沒有什麼難的。推薦使用字串。例如:如果我們想儲存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:)
方法,讓我們用相同的kSecAttrService
和kSecAttrAccount
所對應的值來儲存其他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物件,這個物件包含kSecAttrService
和kSecAttrAccount
。但是這次,我們將會建立另外一個包含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
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
// 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" ```