移动端安全存储:Keychain 与 EncryptedSharedPreferences

FreeGuideOnline 最新 2026-06-17

为什么移动应用需要安全存储

移动端应用常常需要保存用户的敏感信息,例如登录令牌(Token)、密码、API 密钥或支付信息。如果直接以明文方式存入 SharedPreferences(Android)或 UserDefaults(iOS),这些数据极易被逆向工程或设备数据提取工具窃取。安全存储方案正是为了在系统层面提供加密保护,使敏感数据难以被未授权访问。

本教程将带你入门 iOS 平台的 Keychain 和 Android 平台的 EncryptedSharedPreferences,帮助你用平台推荐的方式保护用户数据。


iOS 安全存储:Keychain

Keychain 是苹果提供的硬件级安全存储服务,数据以加密形式存储在设备的专用安全隔区中。相比 UserDefaults,Keychain 更适合保存密码、密钥等少量敏感数据,并且支持在应用卸载后仍可选择保留数据(取决于访问控制)。

Keychain 的基本概念

  • Keychain Services API – C 语言接口,直接与钥匙串交互。
  • Keychain Item – 每条记录称为一个项,通过一个唯一的标识符(如账号名和服务名)进行区分。
  • kSecClass – 定义存储的数据类型,常用 kSecClassGenericPassword(通用密码)或 kSecClassInternetPassword
  • 可访问性(Accessibility) – 通过 kSecAttrAccessible 控制数据在设备锁定、解锁状态下的可访问级别,例如仅解锁时可访问(kSecAttrAccessibleWhenUnlockedThisDeviceOnly)。

使用 Keychain 存储与读取数据

直接使用原生 Keychain Services API 比较繁琐,建议采用轻量封装。以下示例展示一个简单的 Keychain 管理器类。

import Foundation
import Security

class KeychainManager {

    static func save(key: String, value: String) -> Bool {
        guard let valueData = value.data(using: .utf8) else { return false }

        // 构造查询字典,指定服务名与账号
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: "com.yourapp.service",
            kSecAttrAccount as String: key,
            kSecValueData as String: valueData,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
        ]

        // 先删除已存在的同名项
        SecItemDelete(query as CFDictionary)

        // 添加新项
        let status = SecItemAdd(query as CFDictionary, nil)
        return status == errSecSuccess
    }

    static func load(key: String) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: "com.yourapp.service",
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]

        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)

        guard status == errSecSuccess, let data = item as? Data else { return nil }
        return String(data: data, encoding: .utf8)
    }

    static func delete(key: String) -> Bool {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: "com.yourapp.service",
            kSecAttrAccount as String: key
        ]
        let status = SecItemDelete(query as CFDictionary)
        return status == errSecSuccess
    }
}

// 使用示例
let saved = KeychainManager.save(key: "authToken", value: "eyJhbGciOi...")
if let token = KeychainManager.load(key: "authToken") {
    print("读取到令牌: \(token)")
}

Keychain 访问控制与安全建议

  • 使用最高限制级别的可访问性:若数据仅在本设备使用且应用卸载后无需保留,可选用 kSecAttrAccessibleWhenUnlockedThisDeviceOnly。如需后台传输或设备锁定后访问,请谨慎开放权限。
  • 避免存储大量数据:Keychain 适合小型密钥或字符串,大文件应使用文件加密(Data Protection)。
  • 添加应用层加密:对于特别敏感的数据,可以在存入 Keychain 前再进行一次 AES 加密,双重保护。
  • 同步与备份:Keychain 条目默认参与 iCloud 密钥串同步(可通过 kSecAttrSynchronizable 控制)。如果数据是设备特异的,务必关闭同步。

Android 安全存储:EncryptedSharedPreferences

Android 平台在 Jetpack Security 库中提供了 EncryptedSharedPreferences,它基于 SharedPreferences 的接口,但自动对键值对进行加密。内部使用 Android Keystore 系统生成和保存 AES 密钥,达到安全存储的目的。

添加依赖

在模块级 build.gradle.ktsbuild.gradle 中加入:

dependencies {
    implementation "androidx.security:security-crypto:1.1.0-alpha06"
}

创建 EncryptedSharedPreferences 实例

使用 EncryptedSharedPreferences.create() 方法即可得到一个加密的 SharedPreferences 对象。需要提供 MasterKeys 或直接使用默认的密钥生成方案。

import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey

class SecurePrefs(context: Context) {

    private val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()

    val sharedPrefs = EncryptedSharedPreferences.create(
        context,
        "my_secure_prefs",  // 文件名
        masterKey,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )
}

读写操作

与普通 SharedPreferences 完全一致,只需通过 edit() 提交数据。

// 写入加密数据
sharedPrefs.edit()
    .putString("authToken", "eyJhbGciOi...")
    .apply()

// 读取数据
val token = sharedPrefs.getString("authToken", null)

对于更复杂的对象,可以结合 Gson 或 Kotlin Serialization 序列化后存入。

密钥管理与版本考量

  • MasterKey 默认使用 Android Keystore 生成的密钥,攻击者无法在没有 root 权限的情况下导出密钥材料。
  • EncryptedSharedPreferences 会为每个键、值分别加密(可通过加密方案控制),因此具有一定的防篡改能力。
  • 注意:如果使用 setKeyScheme 指定自定义方案,需确保与 Android 版本兼容。AES256_GCM 要求 API 23+。对于更低版本应用,考虑使用第三方安全存储库。

常见问题处理

  1. KeyStore 错误:部分设备制造商实现存在缺陷,可能导致 KeyStoreException。可以通过捕获异常并提示用户,或在 create() 方法中传入预定义的 SharedPreferences 作为降级方案(但会失去加密保护)。
  2. 数据迁移:如果从普通 SharedPreferences 迁移到 EncryptedSharedPreferences,需在升级逻辑中一次性读取明文数据并重新存入加密实例,然后删除原文件。
  3. 备份与还原:加密数据依赖于设备 Keystore,因此在设备间直接复制备份文件通常无效,应使用应用自身的账户系统或安全远程存储密钥。

Keychain 与 EncryptedSharedPreferences 对比

特性 iOS Keychain Android EncryptedSharedPreferences
加密硬件支持 安全隔区,硬件根密钥 基于 Android Keystore(硬件支持因设备而异)
存储类型 数据库(按项查询) 键值对文件(基于 SharedPreferences)
数据保留策略 可控制卸载后保留 随应用删除而被清除
跨应用共享 支持同开发者团队的应用组 不支持直接共享(需 ContentProvider 等)
API 易用性 原生 C API,第三方封装丰富 几乎与 SharedPreferences 用法一致,学习成本低
最低系统版本 iOS 2.0+ Android 6.0+(部分库要求 API 23+)

两者均能安全保护用户凭据,选择时只需遵循各自平台的最佳实践即可。


实践建议与总结

  1. 不要尝试自己实现加密存储:平台提供的方案经历过安全社区的审查和大量测试,自己实现很容易留下漏洞。
  2. 最小化敏感数据的存储时间:令牌和密码在使用后尽快丢弃,仅存储刷新令牌等必要信息。
  3. 结合生物识别增加一层保护:iOS 可设置 Keychain 的可访问属性需要生物认证;Android 可配合 BiometricPrompt 加密/解密数据后再写入 EncryptedSharedPreferences。
  4. 定期轮换密钥:对于长期有效的 API Key,在服务端定期更换,客户端同步更新存储。
  5. 代码混淆与完整性检查:即使数据已加密,也应启用 ProGuard/R8 混淆和 Android 的 SafetyNet/App Check,降低逆向风险。

通过掌握 KeychainEncryptedSharedPreferences,你就能在 iOS 与 Android 应用中以系统级安全强度存储用户的敏感信息,极大提升应用整体的安全性和用户信任度。请立即在你的下一个项目中将明文存储升级为安全存储方案吧!