前言
时下,是一个信息爆炸的时代,信息的传播途径也是层出不穷,市面上出现了各种各样的设备来收集、传播、存储我们的数据,用户的隐私受到了极大的挑战。这些设备中智能手机尤为更胜,做为 iOS 开发者保护用于的数据也是我们应尽的责任。今天我们讨论的是用户登录数据如何免于泄露。
对于用户登录数据的保护,大家首先想到的应该就是 Keychain
, 为了更进一步确保数据安全苹果公司还为我们提供了另外一层安全保护措施——Face ID
和 Touch ID
.
iPhone 5s
以及更新的 iPhone
搭载了A7
及更新的芯片,这些芯片允许您将用户的生物特征数据存储到一个安全的区域。也就是说,我们可以通过Face ID
或 Touch ID
将用户生物特征数据存储到 Keychain
里,从而提高用户数据的安全性。本文中我们将探寻如何使用Face ID
和 Touch ID
来实现登录认证。
正文
创建一个工程,实现一个简单的登录页面(你也可以从这里下载 download), 运行程序如图所示:
打开这个工程,你需要把相应的配置改成你自己的,如:
打开
LoginViewController.swift
, 在
managedObjectContext:
下添加几个常量:
let usernameKey = "Batman"
let passwordKey = "Hello Bruce!"
这是我们内置的用户名和密码,作为校验用户的凭证。让后我们添加一个登录校验函数:
func checkLogin(username: String, password: String) -> Bool {
return username == usernameKey && password == passwordKey
}
接下来实现一个登录函数, 函数内部实现:
if checkLogin(username: usernameTextField.text!, password: passwordTextField.text!) {
performSegue(withIdentifier: "dismissLogin", sender: self)
}
这样一个简单的登录实现就完成了。这种简单的用户校验过程看起来是没有什么问题,实际上我们的用户信息(账号和密码)不应该存储在应用程式中,这样很容易被破解。因此我们可以将其存储到Keychain
中来提高其安全性。如果你想更多的了解Keychain
,请移步 Basic Security in iOS 5 – Part 1.
Keychain
用 Keychain
如何存储数据呢?苹果官方提供了实例代码GenericKeychain
在我们的工程中创建 KeychainPasswordItem.swift
如:
思路:将用户名和密码存入如
Keychain
中,然后在根据用户的输入进行校验。
在使用
Keychain
之前,你需要对应用进行配置保证数据可以写入
Keychain
中,然后打开
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!
我们在声明一个提醒登陆失败的函数:
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)
}
接下来我们登录函数里的实现就可以改为:
@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()
}
}
}
我们的checkLogin(username:password:)
函数的实现也改为:
func checkLogin(username: String, password: String) -> Bool {
guard username == UserDefaults.standard.value(forKey: "username") as? String else {
return false
}
do {
let passwordItem = KeychainPasswordItem(service: KeychainConfiguration.serviceName,
account: username,
accessGroup: KeychainConfiguration.accessGroup)
let keychainPassword = try passwordItem.readPassword()
return password == keychainPassword
} catch {
fatalError("Error reading password from keychain - \(error)")
}
}
删除原来定义的常量代码,实现viewDidLoad()
:
// 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
}
这样我们的Keychain
存储数据就完成了。
那么我们的 Touch ID
呢?
搭建Touch ID
界面(详细过程略):
添加本地验证
Local Authentication
库为我们提供了实现生物特征识别的 Api ,我们在项目中导入 Local Authentication
。官方文档是这样描述的:
“The Local Authentication framework provides facilities for requesting authentication from users with specified security policies.”
安全性提升到直接取决于用户的——手指和脸。在 iOS 11
中开始支持人脸识别, 你需要在工程的 info plist
文件中声明 Privacy – Face ID Usage Description
:
创建
TouchIDAuthentication.swift
文件:
import LocalAuthentication
enum BiometricType {
case none
case touchID
case faceID
}
class BiometricIDAuth {
let context = LAContext()
var loginReason = "Logging in with Touch ID"
func biometricType() -> BiometricType {
let _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
switch context.biometryType {
case .none:
return .none
case .touchID:
return .touchID
case .faceID:
return .faceID
}
}
func canEvaluatePolicy() -> Bool {
return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
}
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 {
// User authenticated successfully, take appropriate action
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) }
}
}
}
你可以在这里下载完整项目project
iOS 安全指南
文献
How To Secure iOS User Data: The Keychain and Biometrics – Face ID or Touch ID