iOS数据持久化KeyChain数据操作详解
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>正文</li><li>保存数据到KeyChain</li><li>更新KeyChain中已有的数据</li><li>从KeyChain中读取数据</li><li>从KeyChain中删除数据</li><li>创建一个通用的KeyChainHelper 类</li></ul></div><p class="maodian"></p><h2>正文</h2><div class="cros igoods"><div class="goodsin" data-img="https://img14.360buyimg.com/pop/jfs/t3229/39/2924280771/392779/d242f52a/57e8d68eN57129bbf.jpg" data-name="精通Swift设计模式(图灵出品)" data-owner="京东自营" data-price="77.4" data-tgid="38" data-url="https://union-click.jd.com/jdc?e=&p=JF8BAMgJK1olXwUFU1xcAUoRA18IG1MTWgIAVm4ZVxNJXF9RXh5UHw0cSgYYXBcIWDoXSQVJQwYCXFhaDEkVHDZNRwYlPwYFKBgUADF0aHV9UlIdXllRFC0ZTkcbM2gNHF4dXwMBZF5eDkwXAmoIK2sVXDZQOobrvpOysnPcsdTA1ZEyVW5dD00TBm4MGl4QWwIHZF5VDHtUVypcWBhdbTYyV25tOEsnAF9WdVpGWwUABlhbZhZCXWpOR15VMwIKXFlVCEonAW4JGVklbQ"></div>
<div class="goodsin" data-img="https://img14.360buyimg.com/pop/jfs/t289/316/201597771/391827/1213e717/54053986Nba3d5ce3.jpg" data-name="Swift开发指南(图灵出品)" data-owner="京东自营" data-price="60" data-tgid="38" data-url="https://union-click.jd.com/jdc?e=&p=JF8BAMgJK1olXwUFU1xcAUoRA18IG18VWgQHU24ZVxNJXF9RXh5UHw0cSgYYXBcIWDoXSQVJQwYCUF5aCk4QHDZNRwYlIFUcVCU5Sxl1VxlSQFJLNHpyLAoqaEcbM2gNHF4dXwMBZF5eDkwXAmoIK2sVXDZQOobrvpOysnPcsdTA1ZEyVW5dD00TBm4MGl4QVQYHZF5VDHtUVypcWBhdbTYyV25tOEsnAF9WdVpGVQYLVQoPZhZCXWpOR1McMwIFU11bCk4nAW4JGVklbQ"></div></div>
<p>在我们开发iOS应用的时候,很多时候,我们都需要将敏感数据(password, accessToken, secretKey等)存储到本地。对于初级程序员来讲,首先映入脑海的可能是使用<code>UserDefaults</code>。然而,众所周知,使用<code>UserDefaults</code>来存储敏感信息简直是low的不能再low的主意了。因为我们一般存储到<code>UserDefaults</code>中的数据都是未经过编码处理的,这样是非常不安全的。</p>
<p>为了能安全的在本地存储敏感信息,我们应当使用苹果提供的<code>KeyChain</code>服务。这个framework已经相当老了,所以,我们在后面阅读的时候,会觉得它提供的API并不像当下的framework那么快捷。</p>
<p>在本文中,将为你展示如何创建一个通用的同时适用于iOS、MacOS的keyChain辅助类,对数据进行增删改查操作。开始吧!!!</p>
<p class="maodian"></p><h2>保存数据到KeyChain</h2>
<div class="jb51code"><pre class="brush:cpp;">final class KeyChainHelper {
static let standard = KeyChainHelper()
private init(){}
}
</pre></div>
<p>我们必须巧妙使用<code>SecItemAdd(_:_:)</code>方法,这个方法会接收一个<code>CFDictionary</code>类型的query对象。</p>
<p>这个主意是为了创建一个query对象,这个对象包含了我们想要存储最主要的数据键值对。然后,将query对象传入<code>SecItemAdd(_:_:)</code>方法中来执行保存操作。</p>
<div class="jb51code"><pre class="brush:cpp;">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)")
}
}
</pre></div>
<p>回看上述代码片段,query对象由4个键值对组成:</p>
<ul><li><code>kSecValueData</code>: 这个键代表着数据已经被存储到了keyChain中</li><li><code>kSecClass</code>: 这个键代表着数据已经被存储到了keyChain中。我们将它的值设为了<code>kSecClassGenericPassword</code>,这代表着我们所保存的数据是一个通用的密码项</li><li><code>kSecAttrService</code>和<code>kSecAttrAccount</code>: 当<code>kSecClass</code>被设置为<code>kSecClassGenericPassword</code>的时候,<code>kSecAttrService</code>和<code>kSecAttrAccount</code>这两个键是必须要有的。这两个键所对应的值将作为所保存数据的关键key,换句话说,我们将使用他们从keyChain中读取所保存的值。</li></ul>
<p>对于<code>kSecAttrService</code>和<code>kSecAttrAccount</code>所对应的值的定义并没有什么难的。推荐使用字符串。例如:如果我们想存储Facebook的accesToken,我们需要将<code>kSecAttrService</code>设置成”access-token“,将<code>kSecAttrAccount</code>设置成”facebook“</p>
<p>创建完query对象之后,我们可以调用<code>SecItemAdd(_:_:)</code>方法来保存数据到keyChain。<code>SecItemAdd(_:_:)</code>方法会返回一个<code>OSStatus</code>来代表存储状态。如果我们得到的是<code>errSecSuccess</code>状态,则意味着数据已经被成功保存到keyChain中</p>
<p>下面是<code>save(_:service:account:)</code>方法的使用</p>
<div class="jb51code"><pre class="brush:cpp;">let accessToken = "dummy-access-token"
let data = Data(accessToken.utf8)
KeychainHelper.standard.save(data, service: "access-token", account: "facebook")
</pre></div>
<p>keyChain不能在playground中使用,所以,上述代码必须写在Controller中。</p>
<p class="maodian"></p><h2>更新KeyChain中已有的数据</h2>
<p>现在我们有了<code>save(_:service:account:)</code>方法,让我们用相同的<code>kSecAttrService</code>和<code>kSecAttrAccount</code>所对应的值来存储其他token</p>
<div class="jb51code"><pre class="brush:cpp;">let accessToken = "another-dummy-access-token"
let data = Data(accessToken.utf8)
KeychainHelper.standard.save(data, service: "access-token", account: "facebook")
</pre></div>
<p>这时候,我们就无法将accessToken保存到keyChain中了。同时,我们会得到一个<code>Error: -25299</code>的报错。该错误码代表的是存储失败。因为我们所使用的keys已经存在于keyChain当中了。</p>
<p>为了解决这个问题,我们需要检查这个错误码(相当于<code>errSecDuplicateItem</code>),然后使用<code>SecItemUpdate(_:_:)</code>方法来更新keyChain。一起看看并更新我们前述的<code>save(_:service:account:)</code>方法吧:</p>
<div class="jb51code"><pre class="brush:cpp;">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 = as CFDictionary
// Update existing item
SecItemUpdate(query, attributesToUpdate)
}
}
</pre></div>
<p>跟保存操作相似的是,我们需要先创建一个query对象,这个对象包含<code>kSecAttrService</code>和<code>kSecAttrAccount</code>。但是这次,我们将会创建另外一个包含<code>kSecValueData</code>的字典,并将它传给<code>SecItemUpdate(_:_:)</code>方法。</p>
<p>这样的话,我们就可以让<code>save(_:service:account:)</code>方法来更新keyChain中已有的数据了。</p>
<p class="maodian"></p><h2>从KeyChain中读取数据</h2>
<p>从keyChain中读取数据的方式和保存的方式非常相似。我们首先要做的是创建一个query对象,然后调用一个keyChain方法:</p>
<div class="jb51code"><pre class="brush:cpp;">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)
}
</pre></div>
<p>跟之前一样,我们需要设置query对象的<code>kSecAttrService</code> and <code>kSecAttrAccount</code>的值。在这之前,我们需要为query对象添加一个新的键<code>kSecReturnData</code>,其值为<code>true</code>,代表的是我们希望query返回对应项的数据。</p>
<p>之后,我们将利用 <code>SecItemCopyMatching(_:_:)</code> 方法并通过引用传入 <code>AnyObject</code> 类型的<code>result</code>对象。<code>SecItemCopyMatching(_:_:)</code>方法同样返回一个OSStatus类型的值,代表读取操作状态。但是如果读取失败了,这里我们不做任何校验,并返回<code>nil</code></p>
<p>让keyChain支持读取的操作就这么多了,看一下他是怎么工作的吧</p>
<div class="jb51code"><pre class="brush:cpp;">let data = KeychainHelper.standard.read(service: "access-token", account: "facebook")!
let accessToken = String(data: data, encoding: .utf8)!
print(accessToken)
</pre></div>
<p class="maodian"></p><h2>从KeyChain中删除数据</h2>
<p>如果没有删除操作,我们的KeyChainHelper类并不算完成。一起看看下面的代码片段吧</p>
<div class="jb51code"><pre class="brush:cpp;">func delete(service: String, account: String) {
let query = [
kSecAttrService: service,
kSecAttrAccount: account,
kSecClass: kSecClassGenericPassword,
] as CFDictionary
// Delete item from keychain
SecItemDelete(query)
}
</pre></div>
<p>如果你全程都在看的话,上述代码可能对你来说非常熟悉,那是相当的”自解释“了,需要注意的是,这里我们使用了<code>SecItemDelete(_:)</code>方法来删除KeyChain中的数据了。</p>
<p class="maodian"></p><h2>创建一个通用的KeyChainHelper 类</h2>
<p>存储</p>
<div class="jb51code"><pre class="brush:cpp;">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)")
}
}
</pre></div>
<p>读取</p>
<div class="jb51code"><pre class="brush:cpp;">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
}
}
</pre></div>
<p>使用</p>
<div class="jb51code"><pre class="brush:cpp;">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"</pre></div>
<p>以上就是iOS数据持久化KeyChain的详细内容,更多关于iOS数据持久化KeyChain的资料请关注琼殿技术社区其它相关文章!</p>
<div class="art_xg">
<b>您可能感兴趣的文章:</b><ul><li>IOS开发自定义Button的外观和交互行为示例详解</li><li>IOS开发Objective-C Runtime使用示例详解</li><li>iOS数据持久化UserDefaults封装器使用详解</li><li>iOS13适配三指撤销和文案限长实例详解</li><li>iOS 16 CocoaAsyncSocket 崩溃修复详解</li><li>iOS开发蓝牙技术应用增加无线连接功能</li></ul>
</div>
</div>
<!--endmain-->
頁:
[1]