Keychain:安全保存登录凭证
Keychain 是系统管理的小型敏感数据存储。它解决凭证放在哪里,不负责判断你的服务器是否仍接受该凭证。
用你的前端经验理解
可以类比一个受系统保护的凭证仓库。UserDefaults 像偏好存储,Keychain 不是加密版业务数据库,也不是登录服务。
为什么需要它
登录凭证需要在应用重启后可恢复,又不能像普通偏好那样随意保存。Keychain 提供系统管理的敏感条目存储,但它不是远端身份认证服务。读取到 refresh token 后,仍要向业务后端换取会话;设备当前无法访问某个条目,也不等于服务端已经撤销账号。
它是怎样工作的
常见 generic password 条目通过 class、service 和 account 等属性确定身份。保存需要处理首次添加与已存在更新,读取要区分不存在、格式不符和系统拒绝。把 OSStatus 映射为可判断的领域错误,比统一返回 nil 更可靠。不同账号应有不同定位条件,避免切换账号时读到旧身份的凭证。
把关键概念连起来
对象与操作
常用 generic password item,以 service 和 account 等属性定位。SecItemAdd、SecItemCopyMatching、SecItemUpdate、SecItemDelete 分别处理增、查、改、删。查询条件要足够精确。
错误与生命周期
返回值是 OSStatus。区分成功、条目不存在、重复条目和当前不可访问;重复时按明确策略更新,失败时不要伪装为“用户尚未登录”。
凭证策略
业务 refresh token 可保存在 Keychain;短期 access token 可按需要留在内存。退出登录时清理本账号对应条目并撤销可撤销的服务端会话,不假定卸载会完成清理。
import Foundationimport Security// 函数内片段:读取这个 service/account 的一条凭证let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "com.example.native.session", kSecAttrAccount as String: "demo-user", kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne]var item: CFTypeRef?let status = SecItemCopyMatching(query as CFDictionary, &item)// status == errSecSuccess:再检查 item 是否为 Data// errSecItemNotFound:没有条目;其他状态应分别处理// 不要记录 item 或 token 原文- 查询中的 service 与 account 共同收窄条目范围;返回数据与匹配数量控制需要取回什么。
- SecItemCopyMatching 返回 OSStatus。只有成功后才能按预期类型解释 item;不存在与暂时不可访问必须分别处理。
用于理解当前概念的代码片段;部分示例需要放入对应工程与作用域,并补齐上下文。
阅读 App 的登录恢复
启动时读取 refresh token,向自己的后端请求新会话。若服务端明确返回已撤销,就清理该账号凭证并进入登录流程;若只是断网,应保留离线内容并显示连接状态。
实现一个可审查的 TokenStore
- save 在条目不存在时添加,重复条目走更新;查询范围限制到当前 service/account,不进行全组覆盖。
- read 返回凭证、无条目或访问错误三类结果。对损坏 Data 的解码失败要单独处理,不把它记录成明文日志。
- 退出时删除本账号条目,并按后端能力撤销会话;失败时记录安全错误信息,避免界面宣称全部清理完成。
这里需要你亲自判断
不要输出 token 原文,也不要在删除时使用过宽查询误删同组的其他凭证;能读取旧 token 不代表旧会话仍有效。
做一个小练习
为一个 token store 定义 save/read/delete 接口;用虚构字符串验证首次保存、更新、读取不存在和退出清理。
- 用虚构 token 测试首次保存、替换、读取不存在、删除后读取与另一个账号不受影响。
- 定义断网与后端明确撤销时不同的登录恢复行为。
展开参考思路与验收标准
无条目可以进入登录,暂时不可访问可等待合适时机,后端明确撤销才按会话策略清理。测试应只使用演示凭证,检查删除条件比检查“返回成功”更重要。