
- 注释掉Podfile中的Alamofire依赖,更新Podfile.lock以反映更改。 - 在yana/APIs/API-README.md中新增自动认证Header机制的详细文档,描述其工作原理、实现细节及最佳实践。 - 在yana/yanaApp.swift中将print语句替换为debugInfo以增强调试信息的输出。 - 在API相关文件中实现用户认证状态检查和相关header的自动添加逻辑,提升API请求的安全性和用户体验。 - 更新多个文件中的日志输出,确保在DEBUG模式下提供详细的调试信息。
362 lines
11 KiB
Swift
362 lines
11 KiB
Swift
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)
|
||
}
|
||
|
||
debugInfo("🔐 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)
|
||
debugInfo("🔐 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:
|
||
debugInfo("🔐 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:
|
||
debugInfo("🔐 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:
|
||
debugInfo("🔐 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()
|
||
debugInfo("🔐 Keychain 中存储的键:")
|
||
for key in keys {
|
||
debugInfo(" - \(key)")
|
||
}
|
||
}
|
||
}
|
||
#endif |