feat: 实现数据迁移和用户信息管理优化
- 在AppDelegate中集成数据迁移管理器,支持从UserDefaults迁移到Keychain。 - 重构UserInfoManager,使用Keychain存储用户信息,增加内存缓存以提升性能。 - 添加API加载效果视图,增强用户体验。 - 更新SplashFeature以支持自动登录和认证状态检查。 - 语言设置迁移至Keychain,确保用户设置的安全性。
This commit is contained in:
362
yana/Utils/Security/KeychainManager.swift
Normal file
362
yana/Utils/Security/KeychainManager.swift
Normal file
@@ -0,0 +1,362 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
/// Keychain 管理器
|
||||
///
|
||||
/// 提供安全的数据存储服务,用于替代 UserDefaults 存储敏感信息。
|
||||
/// 支持任意 Codable 对象的存储和检索。
|
||||
///
|
||||
/// 特性:
|
||||
/// - 数据加密存储在 iOS Keychain 中
|
||||
/// - 支持泛型 Codable 对象
|
||||
/// - 完善的错误处理
|
||||
/// - 线程安全操作
|
||||
/// - 可配置的访问控制级别
|
||||
final class KeychainManager {
|
||||
|
||||
// MARK: - 单例
|
||||
static let shared = KeychainManager()
|
||||
private init() {}
|
||||
|
||||
// MARK: - 配置常量
|
||||
private let service: String = {
|
||||
return Bundle.main.bundleIdentifier ?? "com.yana.app"
|
||||
}()
|
||||
|
||||
private let accessGroup: String? = nil // 可配置 App Group
|
||||
|
||||
// MARK: - 错误类型
|
||||
enum KeychainError: Error, LocalizedError {
|
||||
case dataConversionFailed
|
||||
case encodingFailed(Error)
|
||||
case decodingFailed(Error)
|
||||
case keychainOperationFailed(OSStatus)
|
||||
case itemNotFound
|
||||
case duplicateItem
|
||||
case invalidParameters
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .dataConversionFailed:
|
||||
return "数据转换失败"
|
||||
case .encodingFailed(let error):
|
||||
return "编码失败: \(error.localizedDescription)"
|
||||
case .decodingFailed(let error):
|
||||
return "解码失败: \(error.localizedDescription)"
|
||||
case .keychainOperationFailed(let status):
|
||||
return "Keychain 操作失败: \(status)"
|
||||
case .itemNotFound:
|
||||
return "未找到指定项目"
|
||||
case .duplicateItem:
|
||||
return "项目已存在"
|
||||
case .invalidParameters:
|
||||
return "无效参数"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 访问控制级别
|
||||
enum AccessLevel {
|
||||
case whenUnlocked // 设备解锁时可访问
|
||||
case whenUnlockedThisDeviceOnly // 设备解锁时可访问,不同步到其他设备
|
||||
case afterFirstUnlock // 首次解锁后可访问
|
||||
case afterFirstUnlockThisDeviceOnly // 首次解锁后可访问,不同步
|
||||
|
||||
var attribute: CFString {
|
||||
switch self {
|
||||
case .whenUnlocked:
|
||||
return kSecAttrAccessibleWhenUnlocked
|
||||
case .whenUnlockedThisDeviceOnly:
|
||||
return kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
||||
case .afterFirstUnlock:
|
||||
return kSecAttrAccessibleAfterFirstUnlock
|
||||
case .afterFirstUnlockThisDeviceOnly:
|
||||
return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 存储方法
|
||||
|
||||
/// 存储 Codable 对象到 Keychain
|
||||
/// - Parameters:
|
||||
/// - object: 要存储的对象,必须符合 Codable 协议
|
||||
/// - key: 存储键
|
||||
/// - accessLevel: 访问控制级别,默认为设备解锁时可访问且不同步
|
||||
/// - Throws: KeychainError
|
||||
func store<T: Codable>(_ object: T, forKey key: String, accessLevel: AccessLevel = .whenUnlockedThisDeviceOnly) throws {
|
||||
// 1. 编码对象为 Data
|
||||
let data: Data
|
||||
do {
|
||||
data = try JSONEncoder().encode(object)
|
||||
} catch {
|
||||
throw KeychainError.encodingFailed(error)
|
||||
}
|
||||
|
||||
// 2. 构建查询字典
|
||||
var query = baseQuery(forKey: key)
|
||||
query[kSecValueData] = data
|
||||
query[kSecAttrAccessible] = accessLevel.attribute
|
||||
|
||||
// 3. 删除已存在的项目(如果有)
|
||||
SecItemDelete(query as CFDictionary)
|
||||
|
||||
// 4. 添加新项目
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
|
||||
guard status == errSecSuccess else {
|
||||
throw KeychainError.keychainOperationFailed(status)
|
||||
}
|
||||
|
||||
print("🔐 Keychain 存储成功: \(key)")
|
||||
}
|
||||
|
||||
/// 从 Keychain 检索 Codable 对象
|
||||
/// - Parameters:
|
||||
/// - type: 对象类型
|
||||
/// - key: 存储键
|
||||
/// - Returns: 检索到的对象,如果不存在返回 nil
|
||||
/// - Throws: KeychainError
|
||||
func retrieve<T: Codable>(_ type: T.Type, forKey key: String) throws -> T? {
|
||||
// 1. 构建查询字典
|
||||
var query = baseQuery(forKey: key)
|
||||
query[kSecReturnData] = true
|
||||
query[kSecMatchLimit] = kSecMatchLimitOne
|
||||
|
||||
// 2. 执行查询
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
// 3. 处理查询结果
|
||||
switch status {
|
||||
case errSecSuccess:
|
||||
guard let data = result as? Data else {
|
||||
throw KeychainError.dataConversionFailed
|
||||
}
|
||||
|
||||
// 4. 解码数据
|
||||
do {
|
||||
let object = try JSONDecoder().decode(type, from: data)
|
||||
print("🔐 Keychain 读取成功: \(key)")
|
||||
return object
|
||||
} catch {
|
||||
throw KeychainError.decodingFailed(error)
|
||||
}
|
||||
|
||||
case errSecItemNotFound:
|
||||
return nil
|
||||
|
||||
default:
|
||||
throw KeychainError.keychainOperationFailed(status)
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新 Keychain 中的对象
|
||||
/// - Parameters:
|
||||
/// - object: 新的对象
|
||||
/// - key: 存储键
|
||||
/// - Throws: KeychainError
|
||||
func update<T: Codable>(_ object: T, forKey key: String) throws {
|
||||
// 1. 编码对象
|
||||
let data: Data
|
||||
do {
|
||||
data = try JSONEncoder().encode(object)
|
||||
} catch {
|
||||
throw KeychainError.encodingFailed(error)
|
||||
}
|
||||
|
||||
// 2. 构建查询和更新字典
|
||||
let query = baseQuery(forKey: key)
|
||||
let updateAttributes: [CFString: Any] = [
|
||||
kSecValueData: data
|
||||
]
|
||||
|
||||
// 3. 执行更新
|
||||
let status = SecItemUpdate(query as CFDictionary, updateAttributes as CFDictionary)
|
||||
|
||||
switch status {
|
||||
case errSecSuccess:
|
||||
print("🔐 Keychain 更新成功: \(key)")
|
||||
|
||||
case errSecItemNotFound:
|
||||
// 如果项目不存在,则创建新项目
|
||||
try store(object, forKey: key)
|
||||
|
||||
default:
|
||||
throw KeychainError.keychainOperationFailed(status)
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 Keychain 删除项目
|
||||
/// - Parameter key: 存储键
|
||||
/// - Throws: KeychainError
|
||||
func delete(forKey key: String) throws {
|
||||
let query = baseQuery(forKey: key)
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
|
||||
switch status {
|
||||
case errSecSuccess:
|
||||
print("🔐 Keychain 删除成功: \(key)")
|
||||
|
||||
case errSecItemNotFound:
|
||||
// 项目不存在,视为删除成功
|
||||
break
|
||||
|
||||
default:
|
||||
throw KeychainError.keychainOperationFailed(status)
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查 Keychain 中是否存在指定键的项目
|
||||
/// - Parameter key: 存储键
|
||||
/// - Returns: 是否存在
|
||||
func exists(forKey key: String) -> Bool {
|
||||
var query = baseQuery(forKey: key)
|
||||
query[kSecReturnData] = false
|
||||
query[kSecMatchLimit] = kSecMatchLimitOne
|
||||
|
||||
let status = SecItemCopyMatching(query as CFDictionary, nil)
|
||||
return status == errSecSuccess
|
||||
}
|
||||
|
||||
/// 清除所有应用相关的 Keychain 项目
|
||||
/// - Throws: KeychainError
|
||||
func clearAll() throws {
|
||||
let query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrService: service
|
||||
]
|
||||
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
|
||||
switch status {
|
||||
case errSecSuccess, errSecItemNotFound:
|
||||
print("🔐 Keychain 清除完成")
|
||||
|
||||
default:
|
||||
throw KeychainError.keychainOperationFailed(status)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 私有方法
|
||||
|
||||
/// 构建基础查询字典
|
||||
/// - Parameter key: 存储键
|
||||
/// - Returns: 基础查询字典
|
||||
private func baseQuery(forKey key: String) -> [CFString: Any] {
|
||||
var query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrService: service,
|
||||
kSecAttrAccount: key
|
||||
]
|
||||
|
||||
if let accessGroup = accessGroup {
|
||||
query[kSecAttrAccessGroup] = accessGroup
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 便利方法扩展
|
||||
|
||||
extension KeychainManager {
|
||||
|
||||
/// 存储字符串到 Keychain
|
||||
/// - Parameters:
|
||||
/// - string: 要存储的字符串
|
||||
/// - key: 存储键
|
||||
/// - accessLevel: 访问控制级别
|
||||
/// - Throws: KeychainError
|
||||
func storeString(_ string: String, forKey key: String, accessLevel: AccessLevel = .whenUnlockedThisDeviceOnly) throws {
|
||||
try store(string, forKey: key, accessLevel: accessLevel)
|
||||
}
|
||||
|
||||
/// 从 Keychain 检索字符串
|
||||
/// - Parameter key: 存储键
|
||||
/// - Returns: 检索到的字符串
|
||||
/// - Throws: KeychainError
|
||||
func retrieveString(forKey key: String) throws -> String? {
|
||||
return try retrieve(String.self, forKey: key)
|
||||
}
|
||||
|
||||
/// 存储数据到 Keychain
|
||||
/// - Parameters:
|
||||
/// - data: 要存储的数据
|
||||
/// - key: 存储键
|
||||
/// - accessLevel: 访问控制级别
|
||||
/// - Throws: KeychainError
|
||||
func storeData(_ data: Data, forKey key: String, accessLevel: AccessLevel = .whenUnlockedThisDeviceOnly) throws {
|
||||
var query = baseQuery(forKey: key)
|
||||
query[kSecValueData] = data
|
||||
query[kSecAttrAccessible] = accessLevel.attribute
|
||||
|
||||
SecItemDelete(query as CFDictionary)
|
||||
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
guard status == errSecSuccess else {
|
||||
throw KeychainError.keychainOperationFailed(status)
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 Keychain 检索数据
|
||||
/// - Parameter key: 存储键
|
||||
/// - Returns: 检索到的数据
|
||||
/// - Throws: KeychainError
|
||||
func retrieveData(forKey key: String) throws -> Data? {
|
||||
var query = baseQuery(forKey: key)
|
||||
query[kSecReturnData] = true
|
||||
query[kSecMatchLimit] = kSecMatchLimitOne
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
switch status {
|
||||
case errSecSuccess:
|
||||
return result as? Data
|
||||
case errSecItemNotFound:
|
||||
return nil
|
||||
default:
|
||||
throw KeychainError.keychainOperationFailed(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 调试支持
|
||||
|
||||
#if DEBUG
|
||||
extension KeychainManager {
|
||||
|
||||
/// 列出所有存储的键(仅用于调试)
|
||||
/// - Returns: 所有键的数组
|
||||
func debugListAllKeys() -> [String] {
|
||||
let query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrService: service,
|
||||
kSecReturnAttributes: true,
|
||||
kSecMatchLimit: kSecMatchLimitAll
|
||||
]
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
guard status == errSecSuccess,
|
||||
let items = result as? [[CFString: Any]] else {
|
||||
return []
|
||||
}
|
||||
|
||||
return items.compactMap { item in
|
||||
item[kSecAttrAccount] as? String
|
||||
}
|
||||
}
|
||||
|
||||
/// 打印所有存储的键(仅用于调试)
|
||||
func debugPrintAllKeys() {
|
||||
let keys = debugListAllKeys()
|
||||
print("🔐 Keychain 中存储的键:")
|
||||
for key in keys {
|
||||
print(" - \(key)")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
Reference in New Issue
Block a user