從鑰匙串和生物識別的角度來保護iOS資料
從iPhone 5S開始,生物識別資料就被儲存在採用了64位架構的蘋果A7處理器的裝置上,想登陸你的裝置,你可以輕鬆的使用鑰匙串以及人臉ID或觸控ID完成整個驗證過程。
本文,我將從靜態身份驗證一步一步的講起,講到如何使用鑰匙串來儲存和驗證登入資訊。
在本文中,在大多數情況下,我都是針對觸控ID進行介紹的,不過這個過程也適用於人臉ID,因為其底層的LocalAuthentication框架都是相同的。
測試準備工作
先點此下載一個 ofollow,noindex">基本的筆記應用程式的操作模板 。
操作板上有一個登入檢視,使用者可以輸入使用者名稱和密碼,構建並運行當前的應用狀態:
此時,點選“login”按鈕將簡單的關閉檢視並顯示Note列表,你也可以從此螢幕建立新記錄。點選登出就可以返回到登入檢視。如果應用程式在後臺使用,它會立即返回到登入檢視。
在執行其它操作之前,你應該改變包識別符號(bundle identifier)並建立一個組。選擇TouchMeIn,建立TouchMeIn目標。在General選項中,將Bundle Identifier更改為使用你自己的域名,進行反向域標記 (reverse-domain-notation),例如com.raywenderich.TouchMeIn。
然後,從建立的組的選單中,選擇與你的開發者帳戶關聯的組。
此時,就可以編寫登入程式碼了。
登入程式碼的編寫
此時新增使用者提供的憑證進來,以實施硬編碼。
開啟LoginViewController.swift並在managedObjectContext下方新增以下常量:
let usernameKey = "Batman" let passwordKey = "Hello Bruce!"
這些只是需要編碼的使用者名稱和密碼,你還需檢查使用者提供的憑證。
接下來,在loginAction(_:)中用以下方法進行新增。
func checkLogin(username: String, password: String) -> Bool { return username == usernameKey && password == passwordKey }
此時,你可以根據以前定義的常量檢查使用者當前提供的憑證。接下來,用以下內容替換loginAction(_:)。
if checkLogin(username: usernameTextField.text!, password: passwordTextField.text!) { performSegue(withIdentifier: "dismissLogin", sender: self) }
只要憑證正確,就可以開始呼叫checkLogin(username:password:)。
在本文中,我輸入的使用者名稱和密碼分別是Batman 和Hello Bruce!,登陸後的狀態如下所示。
這種簡單的身份驗證方法雖然可行,但它並不安全,因為儲存為字串的憑證很容易被黑客攻破。最安全的策略是,密碼不應直接儲存在應用程式中。此時,就要用到鑰匙串來儲存密碼了。
目前,大多數應用程式的密碼只是簡單的字串,以bullet的形式隱藏。在應用程式中處理密碼的最佳方式進行SALT或SHA-2方式的加密。
鑰匙串的構建過程
接下來要做的就是在應用程式中新增一個鑰匙串封裝器,如上所述,我會下載一個模板,下載時還會帶一個有用的資原始檔夾。找到並開啟Finder中的資原始檔夾,你會看到KeychainPasswordItem.swift檔案,這個類來自蘋果的樣本程式碼 GenericKeychain 。拉進來,如下所示。
確保Copy items if needed和TouchMeIn選項都被選中:
如果一切順利,現在你就可以利用你的應用程式中的鑰匙串了。
鑰匙串的使用
要使用鑰匙串,你首先要在其中儲存使用者名稱和密碼。接下來,你將檢查使用者提供的憑證,以檢視它們是否與鑰匙串中儲存的使用者名稱和密碼匹配。
你需要跟蹤使用者建立的憑證,以便你可以將“登入”按鈕上的文字從“建立”更改為“登入”。你還會將使用者名稱儲存在使用者預設值中,這樣你就可以在每次執行此檢查時自動執行該過程。
鑰匙串需要一些配置才能正確儲存你的應用程式的資訊,你將以serviceName和可選的accessGroup的形式提供該配置。最終,你會使用一個結構來儲存這些值。
開啟LoginViewController.swift,在匯入語句下方新增以下內容。
// Keychain Configuration struct KeychainConfiguration { static let serviceName = "TouchMeIn" static let accessGroup: String? = nil }
接下來,新增下面的managedObjectContext:
var passwordItems: [KeychainPasswordItem] = [] let createLoginButtonTag = 0 let loginButtonTag = 1 @IBOutlet weak var loginButton: UIButton!
passwordItems是你將傳入鑰匙串的KeychainPasswordItem型別的空陣列,這樣你將使用兩個常量來確定登入按鈕是否被用來建立一些憑證,以便你使用loginButton輸出口來根據其建立狀態來更新登入按鈕的標題。
接下來,會出現兩種情況:如果點選按鈕時,如果使用者還沒有建立憑證,按鈕文字將顯示“建立”;否則按鈕將顯示“登入”。
如果登入失敗,你需要是在checkLogin(username:password:)後新增以下內容:
private func showLoginFailedAlert() { let alertView = UIAlertController(title: "Login Problem", message: "Wrong username or password.", preferredStyle:. alert) let okAction = UIAlertAction(title: "Foiled Again!", style: .default) alertView.addAction(okAction) present(alertView, animated: true) }
現在,將loginAction(sender:)替換為以下內容:
@IBAction func loginAction(sender: UIButton) { // 1 // Check that text has been entered into both the username and password fields. guard let newAccountName = usernameTextField.text, let newPassword = passwordTextField.text, !newAccountName.isEmpty, !newPassword.isEmpty else { showLoginFailedAlert() return } // 2 usernameTextField.resignFirstResponder() passwordTextField.resignFirstResponder() // 3 if sender.tag == createLoginButtonTag { // 4 let hasLoginKey = UserDefaults.standard.bool(forKey: "hasLoginKey") if !hasLoginKey && usernameTextField.hasText { UserDefaults.standard.setValue(usernameTextField.text, forKey: "username") } // 5 do { // This is a new account, create a new keychain item with the account name. let passwordItem = KeychainPasswordItem(service: KeychainConfiguration.serviceName, account: newAccountName, accessGroup: KeychainConfiguration.accessGroup) // Save the password for the new item. try passwordItem.savePassword(newPassword) } catch { fatalError("Error updating keychain - (error)") } // 6 UserDefaults.standard.set(true, forKey: "hasLoginKey") loginButton.tag = loginButtonTag performSegue(withIdentifier: "dismissLogin", sender: self) } else if sender.tag == loginButtonTag { // 7 if checkLogin(username: newAccountName, password: newPassword) { performSegue(withIdentifier: "dismissLogin", sender: self) } else { // 8 showLoginFailedAlert() } } }
那為什麼不把使用者名稱密碼和UserDefaults一起儲存呢?因為儲存在UserDefaults中的值是使用plist檔案儲存的。它本質上是一個駐留在應用程式庫資料夾中的XML檔案,因此任何人都可以讀取裝置的物理訪問。另一方面,鑰匙串使用三重數字加密標準(3DES)來加密其資料。即使有人獲得這些資料,他們也將無法讀取。
接下來,用以下內容替換checkLogin(username:password:)。
let usernameKey = "Batman" let passwordKey = "Hello Bruce!"
此時,需要檢查輸入的使用者名稱是否與UserDefaults中儲存的使用者名稱相匹配,並且密碼與鑰匙串中儲存的密碼是否也相匹配。
刪除以下幾行內容:
// 1 let hasLogin = UserDefaults.standard.bool(forKey: "hasLoginKey") // 2 if hasLogin { loginButton.setTitle("Login", for: .normal) loginButton.tag = loginButtonTag createInfoLabel.isHidden = true } else { loginButton.setTitle("Create", for: .normal) loginButton.tag = createLoginButtonTag createInfoLabel.isHidden = false } // 3 if let storedUsername = UserDefaults.standard.value(forKey: "username") as? String { usernameTextField.text = storedUsername }
根據hasLoginKey的狀態,適當地設定按鈕標題和標籤。將下面的程式碼新增到viewDidLoad()中,然後呼叫super:。
在彈出視窗中,選擇loginButton:
執行時,輸入你自己選擇的使用者名稱和密碼,進行建立。
請注意,如果你忘記連線loginButton 輸出口,那麼你可能會看到錯誤的Fatal error: unexpectedly found nil while unwrapping an Optional value。
現在,點選登出並嘗試使用相同的使用者名稱和密碼登入 ,此時你應該會看到出現的註釋列表。
點選登出並嘗試重新登入,不過這次使用的是不同的密碼,然後點選登入。此時,你應該會看到以下警告:
現在你已經可以使用鑰匙串新增身份驗證了。接下來,就要建立觸控ID了。
觸控ID
人臉ID要求你在物理裝置(如iPhone X)上進行測試,觸控ID現在可以在模擬器中的Xcode 9中模擬。你可以在任何使用A7晶片或更新的裝置和人臉ID / 觸控ID硬體的裝置上測試生物識別ID。
除了使用鑰匙串之外,你還需要將生物識別ID新增到你的專案中。
開啟Assets.xcassets
接下來,從Finder中先前下載的專案中開啟Resources資料夾。找到FaceIcon和Touch-icon-lg.png影象,並將它們拖到Images.xcassets中,它們唯一的差別就是解析度。
開啟Main.storyboard並把物件庫中的按鈕拖動到堆疊檢視中的“建立資訊標籤”下方的Login View Controller Scene中。你可以開啟Document Outline,開啟開合三角標識,並確保Button在堆疊檢視內。如下所示:
在“屬性”檢查器中,調整按鈕的屬性:
1.將型別設定為自定義;
2.將標題設定為空;
3.將影象設定為Touch-icon-lg;
完成後,屬性應如下所示。
選中新的按鈕,然後選擇 “新增新約束”按鈕,將約束條件設定如下。
操作檢視現在應該如下所示:
現在,你仍然在Main.storyboard中,開啟輔助編輯器並確保顯示LoginViewController.swift。接下來,就像在其它輸出口一樣,從你剛新增到LoginViewController.swift的按鈕中選擇控制標記(Control-drag)。
在彈出框中輸入touchIDButton,然後單擊連線。
這樣,你會建立一個輸出口,用於隱藏沒有生物識別ID的裝置上的按鈕。
接下來,為該按鈕新增一個操作過程。把來自同一個按鈕的控制標記拖動到LoginViewController.swift,也就是 checkLogin(username:password:)的上面。
在彈出視窗中,將Connection更改為Action,將Name設定為touchIDLoginAction,現在將Arguments設定為none,然後點選連線,執行以檢查是否有任何錯誤。
新增本地認證
本地身份驗證框架提供了用於請求使用者使用指定的安全策略進行身份驗證的工具,而本文所介紹的安全策略就是使用者的生物識別技術 。
iOS+11/">iOS 11的新功能支援人臉ID,LocalAuthentication添加了一些新的功能:所需的FaceIDUsageDescription和LABiometryType是用來確定裝置是否支援人臉ID或觸控ID。
在Xcode的專案導航器中選擇專案並單擊Info選項,然後單擊+。開始輸入“隱私”,然後在出現的彈出列表中選擇“隱私 – 臉部ID使用說明”,不過你也可以輸入“NSFaceIDUsageDescription”。
這是字串,在value欄位中,你可以使用Face ID來解鎖這些註釋。

在專案導航器中,右鍵單擊TouchMeIn組資料夾並選擇New File,直到找到iOS \ Swift檔案,然後點選下一步。將TouchMeIn目標檔案儲存為TouchIDAuthentication.swift,點選建立。
開啟TouchIDAuthentication.swift並在import Foundation新增以下匯入內容:
import LocalAuthentication
接下來,新增以下內容來建立一個新的類:
class BiometricIDAuth { }
現在你需要引用LAContext類,在花括號之間新增以下程式碼。
let context = LAContext()
用上下文引用認證上下文,這是本地認證的主要特點。如果BiometricIDAuth內部支援生物特徵ID,則新增以下方法以返回Bool:
func canEvaluatePolicy() -> Bool { return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) }
開啟LoginViewController.swift並新增以下屬性以建立對BiometricIDAuth的引用:
let touchMe = BiometricIDAuth()
在viewDidLoad()底部新增如下內容:
touchIDButton.isHidden = !touchMe.canEvaluatePolicy()
在本文,你可以使用canEvaluatePolicy(_:)來檢查裝置是否可以實現生物認證。
面部ID或觸控ID
如果你使用的是iPhone X或更高版本的人臉ID裝置,那就要注意了。因為此時,觸控ID圖示 已經被刪除了。不過,你可以使用biometryType列舉類來解決這個問題,開啟TouchIDAuthentication.swift並在類的上方新增BiometricType列舉。
enum BiometricType { case none case touchID case faceID }
接下來,新增以下函式以使用canEvaluatePolicy返回來選擇支援的生物特徵型別。
func biometricType() -> BiometricType { let _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) switch context.biometryType { case .none: return .none case .touchID: return .touchID case .faceID: return .faceID } }
開啟LoginViewController並將以下內容新增到viewDidLoad()的底部,修復按鈕的圖示:
switch touchMe.biometricType() { case .faceID: touchIDButton.setImage(UIImage(named: "FaceIcon"),for: .normal) default: touchIDButton.setImage(UIImage(named: "Touch-icon-lg"),for: .normal) }
在觸控ID註冊的模擬器上構建並執行,檢視觸控ID圖示,此時你會看到臉部ID圖示顯示在iPhone X上。
開啟TouchIDAuthentication.swift並在上下文中新增以下變數:
var loginReason = "Logging in with Touch ID"
接下來,將下面的方法新增到BiometricIDAuth的底部以對使用者進行身份驗證。
func authenticateUser(completion: @escaping () -> Void) { // 1 // 2 guard canEvaluatePolicy() else { return } // 3 context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: loginReason) { (success, evaluateError) in // 4 if success { DispatchQueue.main.async { // User authenticated successfully, take appropriate action completion() } } else { // TODO: deal with LAError cases } } }
如果使用者通過身份驗證,則可以關閉登入檢視。
錯誤響應
如果你沒有在你的裝置上設定生物識別ID,那該怎麼辦?本地身份驗證的一個重要部分是錯誤響應,所以框架中會包含一個LAError類,不過也有可能是從第二次使用canEvaluatePolicy獲得的一個錯誤。
此時,你會收到一個警告,告訴你問題所在。你需要將TouchIDAuth類的訊息傳遞給LoginViewController。幸運的是,你可以使用完成處理程式來傳遞可選訊息。開啟TouchIDAuthentication.swift並更新authenticateUser方法。更改簽名以包含一個可選的訊息,即遇到錯誤時的提示訊息。
func authenticateUser(completion: @escaping (String?) -> Void) {
接下來,查詢// TODO:並將其替換為以下內容。
// 1 let message: String // 2 switch evaluateError { // 3 case LAError.authenticationFailed?: message = "There was a problem verifying your identity." case LAError.userCancel?: message = "You pressed cancel." case LAError.userFallback?: message = "You pressed password." case LAError.biometryNotAvailable?: message = "Face ID/Touch ID is not available." case LAError.biometryNotEnrolled?: message = "Face ID/Touch ID is not set up." case LAError.biometryLockout?: message = "Face ID/Touch ID is locked." default: message = "Face ID/Touch ID may not be configured" } // 4 completion(message)
完成這些更改後,完成的方法應該如下所示:
func authenticateUser(completion: @escaping (String?) -> Void) { guard canEvaluatePolicy() else { completion("Touch ID not available") return } context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: loginReason) { (success, evaluateError) in if success { DispatchQueue.main.async { completion(nil) } } else { let message: String switch evaluateError { case LAError.authenticationFailed?: message = "There was a problem verifying your identity." case LAError.userCancel?: message = "You pressed cancel." case LAError.userFallback?: message = "You pressed password." case LAError.biometryNotAvailable?: message = "Face ID/Touch ID is not available." case LAError.biometryNotEnrolled?: message = "Face ID/Touch ID is not set up." case LAError.biometryLockout?: message = "Face ID/Touch ID is locked." default: message = "Face ID/Touch ID may not be configured" } completion(message) } } }
編譯錯誤響應時,你會看到三條警告,都是關於常量的。這是由於蘋果增加了對人臉ID的支援,以及Swift匯入Objective-C標頭檔案的方式。
人臉ID
關於iPhone X的最酷的事情之一是使用臉部識別而不用觸控式螢幕幕。你可以通過添加了一個可用於觸發人臉ID的按鈕,但也可以自動觸發人臉ID。
開啟LoginViewController.swift並在viewDidLoad()下面新增如下程式碼:
override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) let touchBool = touchMe.canEvaluatePolicy() if touchBool { touchIDLoginAction() } }
這將驗證你的裝置是否支援生物識別ID,如果支援,則裝置將嘗試驗證使用者。在裝有iPhone X或人臉ID的裝置上構建並執行,以測試執行是否正常。你可以從 這裡 下載完整的示例應用程式。
你在本文中建立的LoginViewController可以為任何需要管理使用者憑證的應用程式提供參考。你還可以新增一個新的檢視控制器,或修改現有的LoginViewController,以允許使用者隨時更改密碼。不過,這對於生物識別ID來說是不必要的,但是,你可以建立一個更新鑰匙串的方法,以提示使用者在修改密碼時輸入當前的密碼。你可以在蘋果的官方iOS安全指南中瞭解更多有關保護你的iOS應用程式的資訊。