导语:从iPhone 5S开始,生物识别数据就被存储在采用了64位架构的苹果A7处理器的设备上,想登陆你的设备,你可以轻松的使用钥匙串以及人脸ID或触摸ID完成整个验证过程。本文,我将从静态身份验证一步一步的讲起,讲到如何使用钥匙串来存储和验
从iPhone 5S开始,生物识别数据就被存储在采用了64位架构的苹果A7处理器的设备上,想登陆你的设备,你可以轻松的使用钥匙串以及人脸ID或触摸ID完成整个验证过程。
本文,我将从静态身份验证一步一步的讲起,讲到如何使用钥匙串来存储和验证登录信息。
在本文中,在大多数情况下,我都是针对触摸ID进行介绍的,不过这个过程也适用于人脸ID,因为其底层的LocalAuthentication框架都是相同的。
测试准备工作
先点此下载一个基本的笔记应用程序的操作模板。
操作板上有一个登录视图,用户可以输入用户名和密码,构建并运行当前的应用状态:
此时,点击“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的新功能支持人脸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应用程序的信息。