You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS/Swift中Keychain保存与读取登录信息功能异常

Keychain 保存/读取功能故障排查与修复

entitlements photo

我开发了一个登录视图,通过人脸识别完成用户身份验证,验证通过后从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
                    }
                }
            }
        }
    }
}

问题分析与修复

核心问题点

  1. kSecAttrAccount 类型错误:该字段应直接存储字符串格式的账号,无需转成Data。原代码将email转成Data存入,读取时又尝试解析为String,类型不匹配导致读取失败。
  2. 读取逻辑的类型解析错误:当kSecReturnAttributes为true时,kSecAttrAccount返回的是原始字符串,而非Data,原代码的类型转换逻辑完全错误。
  3. 缺失错误处理:Keychain操作的返回状态未做完整错误判断,无法定位存储/读取失败的具体原因。
  4. 后台线程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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 20:24:57