游乐游手机版
首页/编程语言/文章详情

iOS KeyChain数据持久化操作详解

时间:2026-06-14 06:48
针对iOS敏感信息本地存储安全问题,使用KeyChain服务加密存储密码、密钥等敏感数据。通过构建单例辅助类,利用SecItemAdd、SecItemUpdate、SecItemCopyMatching和SecItemDelete实现增删改查,并扩展支持Codable类型直接存取,简化操作流程,提升数据安全性与开发效率。

在iOS开发中,敏感信息的本地存储一直是个让人头疼的问题——密码、accessToken、secretKey,随便一个泄露都可能引发安全事故。很多初级开发者下意识会选择UserDefaults,但说实话,用UserDefaults存敏感数据,几乎等于把密码写在便利贴上贴在屏幕边。数据未经编码直接暴露,安全隐患不言而喻。

真正靠谱的做法,是使用苹果提供的KeyChain服务。这个框架虽然有些年头了,API风格也不像当下的框架那么简洁顺手,但它的安全性是经过长期验证的。掌握iOS Keychain安全存储技巧,是每个Swift开发者必备的能力。

本文就来手把手教你创建一个同时适用于iOS和macOS的KeyChain辅助类,涵盖增删改查全套操作。准备好了吗?直接开干!

保存数据到KeyChain

先搭建一个单例辅助类:

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

核心保存操作依赖SecItemAdd(_:_:)方法,它接收一个CFDictionary类型的query对象。思路很简单:构建一个包含待存储数据键值对的query,然后交给SecItemAdd执行。

func sa ve(_ 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对象由四个关键键值对组成:

  • kSecValueData:代表实际要存储的数据
  • kSecClass:这里设为kSecClassGenericPassword,表示存储的是一个通用密码项
  • kSecAttrServicekSecAttrAccount:当kSecClass为通用密码项时,这两个键是必填的。它们组合起来相当于数据的“主键”,后续读取时也要用同样的组合来查找。推荐直接用字符串,比如存Facebook的accessToken时,可以把service设为"access-token",account设为"facebook"

创建完query后,调用SecItemAdd。返回的OSStatus如果等于errSecSuccess,说明保存成功。使用方法如下:

let accessToken = "dummy-access-token"
let data = Data(accessToken.utf8)
KeychainHelper.standard.sa ve(data, service: "access-token", account: "facebook")

注意:KeyChain不能在playground中运行,所以上述代码要写在Controller里才行。

更新KeyChain中已有的数据

现在试试用相同的service和account存另一条token:

let accessToken = "another-dummy-access-token"
let data = Data(accessToken.utf8)
KeychainHelper.standard.sa ve(data, service: "access-token", account: "facebook")

你会遇到一个报错Error: -25299,对应的常量是errSecDuplicateItem。因为相同的key组合已经存在,所以直接添加会失败。解决办法很简单:检测到这个错误码后,改用SecItemUpdate(_:_:)来更新现有条目。我们改造一下sa ve方法:

func sa ve(_ 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)
    }
}

更新操作和保存类似:先构建一个包含service和account的query,再另建一个只包含kSecValueData的字典,传给SecItemUpdate。这样一来,sa ve方法就同时具备了新增和更新的能力,既存则改,未存则加。这是实现KeyChain增删改查的基础逻辑。

从KeyChain中读取数据

读取的思路和保存几乎对称:同样先构建query,然后调用对应的读取方法:

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

注意这里新增了一个kSecReturnData键,值为true,意思是我们希望KeyChain把匹配项的数据返回回来。然后通过SecItemCopyMatching方法,将结果通过引用赋给result。虽然这个方法也会返回一个OSStatus,但为简洁起见,这里省略了错误处理——如果读取失败,直接返回nil

用法如下:

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

从KeyChain中删除数据

没有删除功能的工具类是不完整的。删除比保存和读取更简单:

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

看到这段代码你应该很眼熟了——还是那个熟悉的query配方,然后直接调用SecItemDelete。整个流程非常自解释。通过这个KeyChain辅助类,你可以轻松管理iOS敏感数据存储。

创建一个通用的KeyChainHelper类

上面实现的几个方法只支持Data类型,实际开发中我们经常需要存储对象。如果能直接保存Codable类型就更方便了。下面是升级后的通用版本,让KeyChain支持任意Codable类型:

存储

func sa ve(_ item: T, service: String, account: String) where T : Codable {
    do {
        let data = try JSONEncoder().encode(item)
        sa ve(data, service: service, account: account)
    } catch {
        assertionFailure("Fail to encode item for keychain: (error)")
    }
}

读取

func read(service: String, account: String, type: T.Type) -> T? where T : Codable {
    guard let data = read(service: service, account: account) else {
        return nil
    }
    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
}

let auth = Auth(accessToken: "dummy-access-token",
                 refreshToken: "dummy-refresh-token")
let account = "domain.com"
let service = "token"

// 保存
KeychainHelper.standard.sa ve(auth, service: service, account: account)

// 读取
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"

这样一来,任何满足Codable的结构体或类都可以直接存入KeyChain,而无需手动做Data转换。安全存储敏感信息,这一套组合拳算是彻底拿下了。如果你正在寻找Swift KeyChain教程或iOS Keychain安全存储的最佳实践,这个方案能直接应用到项目中。

来源:https://www.jb51.net/article/274234.htm
上一篇iOS13三指撤销与文案限长适配实例详解 下一篇iOS设计模式之委托模式核心原理与实战解析
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
AWS RDS 数据库配置入门与基础操作指南
编程语言 · 2026-07-10

AWS RDS 数据库配置入门与基础操作指南

本文介绍了AWSRDS的基本概念与核心价值,即提供托管式关系数据库服务,简化运维。详细阐述了创建RDS实例的关键配置步骤,包括引擎选择、实例规格、存储与网络设置。最后,指导读者如何通过多种方式安全连接至数据库实例,并开始进行数据操作,为后续应用开发奠定基础。

PHP MVC中AJAX请求无法调用控制器方法的原因与解决方案
编程语言 · 2026-07-10

PHP MVC中AJAX请求无法调用控制器方法的原因与解决方案

PHPMVC中AJAX请求返回整页HTML的常见原因是控制器方法未正确输出响应或未终止执行,导致框架渲染视图。解决方法是在控制器中设置JSON响应头、输出数据后调用exit()明确终止,同时前端使用小写url和dataType: "json "。

Go语言手动构造rsa.PublicKey:正确初始化大整数模数N完整指南
编程语言 · 2026-07-10

Go语言手动构造rsa.PublicKey:正确初始化大整数模数N完整指南

手动构造RSA公钥时,模数N为*big Int类型,不能直接使用超长十进制字面量,需通过SetString或UnmarshalText方法解析字符串。公钥指数E可直接赋值,推荐65537。生产环境应使用rsa GenerateKey生成密钥对,避免手动构造引发的安全和格式错误。

Go语言实现HTTP定时轮询监控多URL响应时间与状态检测
编程语言 · 2026-07-10

Go语言实现HTTP定时轮询监控多URL响应时间与状态检测

使用Go语言实现HTTP定时轮询监控,通过按行分割与Tab解析URL列表,避免闭包陷阱和nil指针,每个URL启动独立ticker安全并发请求,并配置超时控制与资源关闭,确保响应时间与状态码准确检测。

Tkinter中Label标签在主循环动态更新的正确方法
编程语言 · 2026-07-10

Tkinter中Label标签在主循环动态更新的正确方法

在Tkinter中正确动态更新标签的方法:将标签组件的textvariable参数绑定到一个StringVar变量,然后通过调用该变量的 set()方法更新其值,界面会自动刷新。这样避免直接修改text属性或调用update()。此做法实现数据与界面的解耦,代码更简洁,响应更及时,避免手动同步的闪烁,推荐做法。