版本记录
版本号 | 时间 |
---|---|
V1.0 | 2021.03.30 星期二 |
前言
Authentication Services
框架为用户提供了授权身份认证Authentication
服务,使用户更容易登录App
和服务。下面我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. Authentication Services框架详细解析 (一) —— 基本概览(一)
Overview
为您的应用程序用户提供一种方法来设置帐户并开始使用您的服务。
此示例应用程序Juice
使用Authentication Services框架为用户提供了一个设置帐户并使用其Apple ID
登录的界面。 该应用程序显示一种表单,用户可以在其中创建和设置该应用程序的帐户,然后通过Sign in with Apple
来验证用户的Apple ID
并显示用户的帐户数据。
有关在iOS 12
及更早版本上实施Sign in with Apple
的更多信息,请参阅 Incorporating Sign in with Apple into Other Platforms。
Configure the Sample Code Project
要配置示例代码项目,请在Xcode
中执行以下步骤:
- 1) 在
Signing & Capabilities
窗格上,将set the bundle ID设置为唯一标识符(您必须更改bundle ID
才能继续)。 - 2) Add your Apple ID account并将assign the target to a team,以便Xcode可以通过您的配置文件
provisioning profile
启用Sign in with Apple
功能。 - 3) 从
scheme
弹出菜单中选择一个运行目标,该目标已使用Apple ID
登录并使用Two-Factor Authentication
。 - 4) 如有必要,请在
Signing & Capabilities
窗格中单击Register Device
以创建配置文件。 - 5) 在工具栏中,单击运行,或选择
Product > Run (⌘R)
。
Add a Sign in with Apple Button
在示例应用程序中,LoginViewController
在其视图层次结构中显示一个登录表单和一个Sign in with Apple
按钮(ASAuthorizationAppleIDButton)。 视图控制器还将自身添加为按钮的target
,并传递一个在按钮收到触摸事件时要调用的操作。
func setupProviderLoginView() {
let authorizationButton = ASAuthorizationAppleIDButton()
authorizationButton.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress), for: .touchUpInside)
self.loginProviderStackView.addArrangedSubview(authorizationButton)
}
重要:将
Sign in with Apple
按钮添加到storyboard
中时,还必须在Xcode
的Identity Inspector
中将控件的类值设置为ASAuthorizationAppleIDButton
。
Request Authorization with Apple ID
当用户点击Sign in with Apple
按钮时,视图控制器将调用handleAuthorizationAppleIDButtonPress()
函数,该函数通过对用户的全名和电子邮件地址执行授权请求来启动身份验证流程。 然后,系统检查用户是否使用设备上的Apple ID
登录。 如果用户未在系统级别登录,则该应用会显示一条alert
,指导用户使用其Apple ID
在“设置” Settings
中登录。
@objc
func handleAuthorizationAppleIDButtonPress() {
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.presentationContextProvider = self
authorizationController.performRequests()
}
重要:用户必须启用
Two-Factor Authentication
才能使用Sign in with Apple
,以便安全地访问帐户。
授权控制器调用 presentationAnchorForAuthorizationController: 函数,以从应用程序获取窗口,在该窗口中,以modal sheet
的形式向用户显示Sign in with Apple
的信息。
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return self.view.window!
}
如果用户使用他们的Apple ID
在系统级别登录,则会出现说明Sign in with Apple
功能的sheet
,然后是另一个sheet
,允许用户编辑其帐户中的信息。 用户可以编辑自己的名字和姓氏,选择其他电子邮件地址作为他们的联系信息,并在应用程序中隐藏其电子邮件地址。 如果用户选择从应用程序隐藏其电子邮件地址,则Apple
会生成一个代理电子邮件地址,以将电子邮件转发到用户的私人电子邮件地址。 最后,用户输入Apple ID
的密码,然后单击Continue
创建帐户。
Handle User Credentials
如果身份验证成功,则授权控制器将调用 authorizationController:didCompleteWithAuthorization:`代理函数,应用程序将其用于将用户数据存储在钥匙串中。
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
switch authorization.credential {
case let appleIDCredential as ASAuthorizationAppleIDCredential:
// Create an account in your system.
let userIdentifier = appleIDCredential.user
let fullName = appleIDCredential.fullName
let email = appleIDCredential.email
// For the purpose of this demo app, store the `userIdentifier` in the keychain.
self.saveUserInKeychain(userIdentifier)
// For the purpose of this demo app, show the Apple ID credential information in the `ResultViewController`.
self.showResultViewController(userIdentifier: userIdentifier, fullName: fullName, email: email)
case let passwordCredential as ASPasswordCredential:
// Sign in using an existing iCloud Keychain credential.
let username = passwordCredential.user
let password = passwordCredential.password
// For the purpose of this demo app, show the password credential as an alert.
DispatchQueue.main.async {
self.showPasswordCredentialAlert(username: username, password: password)
}
default:
break
}
}
注意:在您的实现中,
ASAuthorizationControllerDelegate.authorizationController(controller:didCompleteWithAuthorization :)
代理函数应使用用户标识符中包含的数据在系统中创建一个帐户。
如果身份验证失败,则授权控制器将调用authorizationController:didCompleteWithError:代理函数来处理错误。
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
// Handle error.
}
系统对用户进行身份验证后,应用程序将显示ResultViewController
,其中显示了从框架请求的用户信息,包括用户提供的全名和电子邮件地址。 视图控制器还显示Sign Out
按钮,并将用户数据存储在钥匙串中。 当用户点击Sign Out
按钮时,该应用将从视图控制器和钥匙串中删除用户信息,并将LoginViewController
呈现给用户。
Request Existing Credentials
LoginViewController.performExistingAccountSetupFlows()
函数通过请求Apple ID
和iCloud
钥匙串密码来检查用户是否具有现有帐户。 与handleAuthorizationAppleIDButtonPress()
相似,授权控制器设置其presentation content provider
和代理给LoginViewController
对象。
func performExistingAccountSetupFlows() {
// Prepare requests for both Apple ID and password providers.
let requests = [ASAuthorizationAppleIDProvider().createRequest(),
ASAuthorizationPasswordProvider().createRequest()]
// Create an authorization controller with the given requests.
let authorizationController = ASAuthorizationController(authorizationRequests: requests)
authorizationController.delegate = self
authorizationController.presentationContextProvider = self
authorizationController.performRequests()
}
authorizationController(controller:didCompleteWithAuthorization :)
委托函数检查凭据是Apple ID
(ASAuthorizationAppleIDCredential)
还是密码凭据(ASPasswordCredential)
。如果该凭证是密码凭证,则系统显示alert
,允许用户使用现有帐户进行身份验证。
Check User Credentials at Launch
该示例应用程序仅在必要时显示Sign in with Apple
用户界面。应用程序委托在启动后立即在AppDelegate.application(_:didFinishLaunchingWithOptions :)
函数中检查已保存用户凭据的状态。
getCredentialStateForUserID:completion:函数检索保存在钥匙串中的用户标识符的状态。如果用户授予了该应用程序的授权(例如,该用户使用其在设备上的Apple ID
登录到该应用程序),则该应用程序将继续执行。如果用户撤销了对应用程序的授权,或者找不到用户的凭据状态,则该应用程序将通过调用showLoginViewController()
函数来显示登录表单。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let appleIDProvider = ASAuthorizationAppleIDProvider()
appleIDProvider.getCredentialState(forUserID: KeychainItem.currentUserIdentifier) { (credentialState, error) in
switch credentialState {
case .authorized:
break // The Apple ID credential is valid.
case .revoked, .notFound:
// The Apple ID credential is either revoked or was not found, so show the sign-in UI.
DispatchQueue.main.async {
self.window?.rootViewController?.showLoginViewController()
}
default:
break
}
}
return true
}
源码
下面就一起看一下源码,首先还是先看一下示例工程组织结构:
接着,就是源码了
1. AppDelegate.swift
import UIKit
import AuthenticationServices
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
/// - Tag: did_finish_launching
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let appleIDProvider = ASAuthorizationAppleIDProvider()
appleIDProvider.getCredentialState(forUserID: KeychainItem.currentUserIdentifier) { (credentialState, error) in
switch credentialState {
case .authorized:
break // The Apple ID credential is valid.
case .revoked, .notFound:
// The Apple ID credential is either revoked or was not found, so show the sign-in UI.
DispatchQueue.main.async {
self.window?.rootViewController?.showLoginViewController()
}
default:
break
}
}
return true
}
}
2. LoginViewController.swift
import UIKit
import AuthenticationServices
class LoginViewController: UIViewController {
@IBOutlet weak var loginProviderStackView: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
setupProviderLoginView()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
performExistingAccountSetupFlows()
}
/// - Tag: add_appleid_button
func setupProviderLoginView() {
let authorizationButton = ASAuthorizationAppleIDButton()
authorizationButton.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress), for: .touchUpInside)
self.loginProviderStackView.addArrangedSubview(authorizationButton)
}
// - Tag: perform_appleid_password_request
/// Prompts the user if an existing iCloud Keychain credential or Apple ID credential is found.
func performExistingAccountSetupFlows() {
// Prepare requests for both Apple ID and password providers.
let requests = [ASAuthorizationAppleIDProvider().createRequest(),
ASAuthorizationPasswordProvider().createRequest()]
// Create an authorization controller with the given requests.
let authorizationController = ASAuthorizationController(authorizationRequests: requests)
authorizationController.delegate = self
authorizationController.presentationContextProvider = self
authorizationController.performRequests()
}
/// - Tag: perform_appleid_request
@objc
func handleAuthorizationAppleIDButtonPress() {
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.presentationContextProvider = self
authorizationController.performRequests()
}
}
extension LoginViewController: ASAuthorizationControllerDelegate {
/// - Tag: did_complete_authorization
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
switch authorization.credential {
case let appleIDCredential as ASAuthorizationAppleIDCredential:
// Create an account in your system.
let userIdentifier = appleIDCredential.user
let fullName = appleIDCredential.fullName
let email = appleIDCredential.email
// For the purpose of this demo app, store the `userIdentifier` in the keychain.
self.saveUserInKeychain(userIdentifier)
// For the purpose of this demo app, show the Apple ID credential information in the `ResultViewController`.
self.showResultViewController(userIdentifier: userIdentifier, fullName: fullName, email: email)
case let passwordCredential as ASPasswordCredential:
// Sign in using an existing iCloud Keychain credential.
let username = passwordCredential.user
let password = passwordCredential.password
// For the purpose of this demo app, show the password credential as an alert.
DispatchQueue.main.async {
self.showPasswordCredentialAlert(username: username, password: password)
}
default:
break
}
}
private func saveUserInKeychain(_ userIdentifier: String) {
do {
try KeychainItem(service: "com.example.apple-samplecode.juice", account: "userIdentifier").saveItem(userIdentifier)
} catch {
print("Unable to save userIdentifier to keychain.")
}
}
private func showResultViewController(userIdentifier: String, fullName: PersonNameComponents?, email: String?) {
guard let viewController = self.presentingViewController as? ResultViewController
else { return }
DispatchQueue.main.async {
viewController.userIdentifierLabel.text = userIdentifier
if let givenName = fullName?.givenName {
viewController.givenNameLabel.text = givenName
}
if let familyName = fullName?.familyName {
viewController.familyNameLabel.text = familyName
}
if let email = email {
viewController.emailLabel.text = email
}
self.dismiss(animated: true, completion: nil)
}
}
private func showPasswordCredentialAlert(username: String, password: String) {
let message = "The app has received your selected credential from the keychain. \n\n Username: \(username)\n Password: \(password)"
let alertController = UIAlertController(title: "Keychain Credential Received",
message: message,
preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
/// - Tag: did_complete_error
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
// Handle error.
}
}
extension LoginViewController: ASAuthorizationControllerPresentationContextProviding {
/// - Tag: provide_presentation_anchor
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return self.view.window!
}
}
extension UIViewController {
func showLoginViewController() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let loginViewController = storyboard.instantiateViewController(withIdentifier: "loginViewController") as? LoginViewController {
loginViewController.modalPresentationStyle = .formSheet
loginViewController.isModalInPresentation = true
self.present(loginViewController, animated: true, completion: nil)
}
}
}
3. ResultViewController.swift
import UIKit
import AuthenticationServices
class ResultViewController: UIViewController {
@IBOutlet weak var userIdentifierLabel: UILabel!
@IBOutlet weak var givenNameLabel: UILabel!
@IBOutlet weak var familyNameLabel: UILabel!
@IBOutlet weak var emailLabel: UILabel!
@IBOutlet weak var signOutButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
userIdentifierLabel.text = KeychainItem.currentUserIdentifier
}
@IBAction func signOutButtonPressed() {
// For the purpose of this demo app, delete the user identifier that was previously stored in the keychain.
KeychainItem.deleteUserIdentifierFromKeychain()
// Clear the user interface.
userIdentifierLabel.text = ""
givenNameLabel.text = ""
familyNameLabel.text = ""
emailLabel.text = ""
// Display the login controller again.
DispatchQueue.main.async {
self.showLoginViewController()
}
}
}
4. KeychainItem.swift
import Foundation
struct KeychainItem {
// MARK: Types
enum KeychainError: Error {
case noPassword
case unexpectedPasswordData
case unexpectedItemData
case unhandledError
}
// MARK: Properties
let service: String
private(set) var account: String
let accessGroup: String?
// MARK: Intialization
init(service: String, account: String, accessGroup: String? = nil) {
self.service = service
self.account = account
self.accessGroup = accessGroup
}
// MARK: Keychain access
func readItem() throws -> String {
/*
Build a query to find the item that matches the service, account and
access group.
*/
var query = KeychainItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
query[kSecMatchLimit as String] = kSecMatchLimitOne
query[kSecReturnAttributes as String] = kCFBooleanTrue
query[kSecReturnData as String] = kCFBooleanTrue
// Try to fetch the existing keychain item that matches the query.
var queryResult: AnyObject?
let status = withUnsafeMutablePointer(to: &queryResult) {
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
}
// Check the return status and throw an error if appropriate.
guard status != errSecItemNotFound else { throw KeychainError.noPassword }
guard status == noErr else { throw KeychainError.unhandledError }
// Parse the password string from the query result.
guard let existingItem = queryResult as? [String: AnyObject],
let passwordData = existingItem[kSecValueData as String] as? Data,
let password = String(data: passwordData, encoding: String.Encoding.utf8)
else {
throw KeychainError.unexpectedPasswordData
}
return password
}
func saveItem(_ password: String) throws {
// Encode the password into an Data object.
let encodedPassword = password.data(using: String.Encoding.utf8)!
do {
// Check for an existing item in the keychain.
try _ = readItem()
// Update the existing item with the new password.
var attributesToUpdate = [String: AnyObject]()
attributesToUpdate[kSecValueData as String] = encodedPassword as AnyObject?
let query = KeychainItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
// Throw an error if an unexpected status was returned.
guard status == noErr else { throw KeychainError.unhandledError }
} catch KeychainError.noPassword {
/*
No password was found in the keychain. Create a dictionary to save
as a new keychain item.
*/
var newItem = KeychainItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
newItem[kSecValueData as String] = encodedPassword as AnyObject?
// Add a the new item to the keychain.
let status = SecItemAdd(newItem as CFDictionary, nil)
// Throw an error if an unexpected status was returned.
guard status == noErr else { throw KeychainError.unhandledError }
}
}
func deleteItem() throws {
// Delete the existing item from the keychain.
let query = KeychainItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
let status = SecItemDelete(query as CFDictionary)
// Throw an error if an unexpected status was returned.
guard status == noErr || status == errSecItemNotFound else { throw KeychainError.unhandledError }
}
// MARK: Convenience
private static func keychainQuery(withService service: String, account: String? = nil, accessGroup: String? = nil) -> [String: AnyObject] {
var query = [String: AnyObject]()
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrService as String] = service as AnyObject?
if let account = account {
query[kSecAttrAccount as String] = account as AnyObject?
}
if let accessGroup = accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup as AnyObject?
}
return query
}
/*
For the purpose of this demo app, the user identifier will be stored in the device keychain.
You should store the user identifier in your account management system.
*/
static var currentUserIdentifier: String {
do {
let storedIdentifier = try KeychainItem(service: "com.example.apple-samplecode.juice", account: "userIdentifier").readItem()
return storedIdentifier
} catch {
return ""
}
}
static func deleteUserIdentifierFromKeychain() {
do {
try KeychainItem(service: "com.example.apple-samplecode.juice", account: "userIdentifier").deleteItem()
} catch {
print("Unable to delete userIdentifier from keychain")
}
}
}
后记
本篇主要讲述了使用
Sign in with Apple
实现用户身份验证,感兴趣的给个赞或者关注~~~