iOS/Swift中Keychain保存与读取登录信息功能异常
Keychain 保存/读取功能故障排查与修复

我开发了一个登录视图,通过人脸识别完成用户身份验证,验证通过后从Keychain读取已保存的登录信息。目前该功能完全无法正常运行,身份验证功能正常,但保存和读取Keychain的逻辑存在问题,查阅相关资料后仍未解决。
原代码
import SwiftUI import LocalAuthentication import AuthenticationServices struct LogInView: View { @State private var email = "" @State private var password = "" var body: some View { ZStack(alignment: .top){ GeometryReader { geometry in VStack { HStack{ Button { save(email: email, password: password) } label: { Text("Save password") } }.padding(.top, 10) VStack(spacing: 15){ CustomTextField(imageName: "envelope", placeHolderText: "Email", text: $email) CustomTextField(imageName: "lock", placeHolderText: "Password", isSecureField: true, text: $password) } } } } .onAppear(perform: authenticate) } func save(email: String, password: String) { let emailData = email.data(using: .utf8)! let passwordData = password.data(using: .utf8)! let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, kSecAttrService as String: "https://hustle.page", kSecAttrAccount as String: emailData, kSecValueData as String: passwordData ] let saveStatus = SecItemAdd(query as CFDictionary, nil) if saveStatus == errSecDuplicateItem { update(email: email, password: password) } } func update(email: String, password: String) { let emailData = email.data(using: .utf8)! let passwordData = password.data(using: .utf8)! let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, kSecAttrService as String: "https://hustle.page", kSecAttrAccount as String: emailData ] let updatedData: [String: Any] = [ kSecValueData as String: passwordData ] SecItemUpdate(query as CFDictionary, updatedData as CFDictionary) } func read(service: String) -> (String, String)? { let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, kSecAttrService as String: service, kSecReturnAttributes as String: true, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitAll ] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) if status == errSecSuccess, let items = result as? [[String: Any]], let item = items.first { if let account = item[kSecAttrAccount as String] as? String, let passwordData = item[kSecValueData as String] as? Data, let password = String(data: passwordData, encoding: .utf8) { return (account, password) } } return nil } func authenticate() { let context = LAContext() var error: NSError? if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { let reason = "Secure Authentication." context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in if success { if let loginInfo = read(service: "https://hustle.page") { let (email, password) = loginInfo } } } } } }
问题分析与修复
核心问题点
kSecAttrAccount类型错误:该字段应直接存储字符串格式的账号,无需转成Data。原代码将email转成Data存入,读取时又尝试解析为String,类型不匹配导致读取失败。- 读取逻辑的类型解析错误:当
kSecReturnAttributes为true时,kSecAttrAccount返回的是原始字符串,而非Data,原代码的类型转换逻辑完全错误。 - 缺失错误处理:Keychain操作的返回状态未做完整错误判断,无法定位存储/读取失败的具体原因。
- 后台线程UI更新风险:生物识别回调在后台线程执行,读取Keychain后更新UI需切换到主线程。
修正后的代码
import SwiftUI import LocalAuthentication import Security // 替换AuthenticationServices,Keychain操作依赖Security框架 struct LogInView: View { @State private var email = "" @State private var password = "" private let serviceIdentifier = "https://hustle.page" var body: some View { ZStack(alignment: .top){ GeometryReader { geometry in VStack { HStack{ Button { save(email: email, password: password) } label: { Text("Save password") } }.padding(.top, 10) VStack(spacing: 15){ CustomTextField(imageName: "envelope", placeHolderText: "Email", text: $email) CustomTextField(imageName: "lock", placeHolderText: "Password", isSecureField: true, text: $password) } } } } .onAppear(perform: authenticate) } func save(email: String, password: String) { guard !email.isEmpty, !password.isEmpty else { return } let passwordData = password.data(using: .utf8)! let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, kSecAttrService as String: serviceIdentifier, kSecAttrAccount as String: email, // 直接存储字符串账号 kSecValueData as String: passwordData ] let saveStatus = SecItemAdd(query as CFDictionary, nil) switch saveStatus { case errSecSuccess: print("账号密码保存成功") case errSecDuplicateItem: update(email: email, password: password) default: print("保存失败,错误码:\(saveStatus)") } } func update(email: String, password: String) { guard !email.isEmpty, !password.isEmpty else { return } let passwordData = password.data(using: .utf8)! let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, kSecAttrService as String: serviceIdentifier, kSecAttrAccount as String: email ] let updatedData: [String: Any] = [ kSecValueData as String: passwordData ] let updateStatus = SecItemUpdate(query as CFDictionary, updatedData as CFDictionary) if updateStatus != errSecSuccess { print("更新失败,错误码:\(updateStatus)") } else { print("账号密码更新成功") } } func read(service: String) -> (String, String)? { let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, kSecAttrService as String: service, kSecReturnAttributes as String: true, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitFirst // 只取第一条匹配项即可 ] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) guard status == errSecSuccess, let item = result as? [String: Any] else { print("读取失败,错误码:\(status)") return nil } // 直接从item中取出字符串账号 guard let account = item[kSecAttrAccount as String] as? String, let passwordData = item[kSecValueData as String] as? Data, let password = String(data: passwordData, encoding: .utf8) else { print("解析账号密码失败") return nil } return (account, password) } func authenticate() { let context = LAContext() var error: NSError? if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { let reason = "Secure Authentication." context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in DispatchQueue.main.async { // 切回主线程更新UI if success { if let loginInfo = read(service: self.serviceIdentifier) { self.email = loginInfo.0 self.password = loginInfo.1 } } else { print("身份验证失败:\(authenticationError?.localizedDescription ?? "未知错误")") } } } } else { print("设备不支持生物识别:\(error?.localizedDescription ?? "未知错误")") } } } // 补充CustomTextField实现(原代码未提供) struct CustomTextField: View { let imageName: String let placeHolderText: String var isSecureField = false @Binding var text: String var body: some View { HStack { Image(systemName: imageName) .foregroundColor(.gray) if isSecureField { SecureField(placeHolderText, text: $text) } else { TextField(placeHolderText, text: $text) } } .padding() .background(Color(.systemGray6)) .cornerRadius(8) } }
额外说明
- 替换了
AuthenticationServices为Security框架:Keychain操作的API属于Security框架,原导入的AuthenticationServices是用于Apple登录等功能的,和当前场景无关。 - 增加了空值判断:避免空账号密码存入Keychain。
- 优化了读取逻辑:使用
kSecMatchLimitFirst替代kSecMatchLimitAll,因为单个服务+账号只会有一条记录,无需返回所有匹配项。 - 主线程更新UI:生物识别回调在后台队列,必须切回主线程才能更新@State变量。
内容的提问来源于stack exchange,提问作者Ahmed Zaidan
相关产品推荐
相关产品推荐

