在前一篇我们有简单介绍了 Sign in with Apple 是什麽,有哪些使用限制
以及完成了 Sign in with Apple 的前置作业
像是在 Xcode 内新增 Sign in with Apple 的 Capability、Firebase Auth 内启用 Apple 登入
这篇我们要来将功能实作出来~
首先先引入 Firebase Auth、AuthenticationServices、CryptoKit 这三个
import FirebaseAuth // 用来与 Firebase Auth 进行串接用的
import AuthenticationServices // Sign in with Apple 的主体框架
import CryptoKit // 用来产生随机字串 (Nonce) 的
接着我们来建立一个 Sign in with Apple 的按钮,并且可以根据系统模式来变更显示颜色
浅色模式就显示黑色的,深色模式就显示白色的
按钮样式可以参考 Apple 官方文件:Buttons
或者是可以透过 Apple 官方提供的线上设计来模拟:Sign in with Apple Button
override func viewDidLoad() {
super.viewDidLoad()
setSignInWithAppleBtn()
}
// MARK: - 在画面上产生 Sign in with Apple 按钮
func setSignInWithAppleBtn() {
let signInWithAppleBtn = ASAuthorizationAppleIDButton(authorizationButtonType: .signIn, authorizationButtonStyle: chooseAppleButtonStyle())
view.addSubview(signInWithAppleBtn)
signInWithAppleBtn.cornerRadius = 25
signInWithAppleBtn.addTarget(self, action: #selector(signInWithApple), for: .touchUpInside)
signInWithAppleBtn.translatesAutoresizingMaskIntoConstraints = false
signInWithAppleBtn.heightAnchor.constraint(equalToConstant: 50).isActive = true
signInWithAppleBtn.widthAnchor.constraint(equalToConstant: 280).isActive = true
signInWithAppleBtn.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
signInWithAppleBtn.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -70).isActive = true
}
func chooseAppleButtonStyle() -> ASAuthorizationAppleIDButton.Style {
return (UITraitCollection.current.userInterfaceStyle == .light) ? .black : .white // 浅色模式就显示黑色的按钮,深色模式就显示白色的按钮
}
接下来是 请求登入的动作
// MARK: - Sign in with Apple 登入
fileprivate var currentNonce: String?
@objc func signInWithApple() {
let nonce = randomNonceString()
currentNonce = nonce
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
request.nonce = sha256(nonce)
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.presentationContextProvider = self
authorizationController.performRequests()
}
我们需要为每个请求登入时都产生一个随机字串 (Nonce)
来确保说我们取得的每个 ID Token 都是只用来进行该 App 的身份验证请求使用
这个对於防止 Replay attacks (重送攻击) 是很重要的
private func randomNonceString(length: Int = 32) -> String {
precondition(length > 0)
let charset: Array<Character> = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
var result = ""
var remainingLength = length
while(remainingLength > 0) {
let randoms: [UInt8] = (0 ..< 16).map { _ in
var random: UInt8 = 0
let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
if (errorCode != errSecSuccess) {
fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)")
}
return random
}
randoms.forEach { random in
if (remainingLength == 0) {
return
}
if (random < charset.count) {
result.append(charset[Int(random)])
remainingLength -= 1
}
}
}
return result
}
private func sha256(_ input: String) -> String {
let inputData = Data(input.utf8)
let hashedData = SHA256.hash(data: inputData)
let hashString = hashedData.compactMap {
return String(format: "%02x", $0)
}.joined()
return hashString
}
接下来是实作 ASAuthorizationControllerDelegate 的环节
这个环节是用来进行登入成功与登入失败的逻辑处理
extension SignInWithAppleVC: ASAuthorizationControllerDelegate {
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
// 登入成功
if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
guard let nonce = currentNonce else {
fatalError("Invalid state: A login callback was received, but no login request was sent.")
}
guard let appleIDToken = appleIDCredential.identityToken else {
CustomFunc.customAlert(title: "", message: "Unable to fetch identity token", vc: self, actionHandler: nil)
return
}
guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
CustomFunc.customAlert(title: "", message: "Unable to serialize token string from data\n\(appleIDToken.debugDescription)", vc: self, actionHandler: nil)
return
}
// 产生 Apple ID 登入的 Credential
let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce)
// 与 Firebase Auth 进行串接
firebaseSignInWithApple(credential: credential)
}
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
// 登入失败,处理 Error
switch error {
case ASAuthorizationError.canceled:
CustomFunc.customAlert(title: "使用者取消登入", message: "", vc: self, actionHandler: nil)
break
case ASAuthorizationError.failed:
CustomFunc.customAlert(title: "授权请求失败", message: "", vc: self, actionHandler: nil)
break
case ASAuthorizationError.invalidResponse:
CustomFunc.customAlert(title: "授权请求无回应", message: "", vc: self, actionHandler: nil)
break
case ASAuthorizationError.notHandled:
CustomFunc.customAlert(title: "授权请求未处理", message: "", vc: self, actionHandler: nil)
break
case ASAuthorizationError.unknown:
CustomFunc.customAlert(title: "授权失败,原因不知", message: "", vc: self, actionHandler: nil)
break
default:
break
}
}
}
接下来是实作 ASAuthorizationControllerPresentationContextProviding 的环节
这个环节是用来告诉说要在哪个画面上呈现授权画面
// MARK: - ASAuthorizationControllerPresentationContextProviding
// 在画面上显示授权画面
extension SignInWithAppleVC: ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return view.window!
}
}
接下来是透过 Credential 与 Firebase Auth 进行串接
extension SignInWithAppleVC {
// MARK: - 透过 Credential 与 Firebase Auth 串接
func firebaseSignInWithApple(credential: AuthCredential) {
Auth.auth().signIn(with: credential) { authResult, error in
guard error == nil else {
CustomFunc.customAlert(title: "", message: "\(String(describing: error!.localizedDescription))", vc: self, actionHandler: nil)
return
}
CustomFunc.customAlert(title: "登入成功!", message: "", vc: self, actionHandler: self.getFirebaseUserInfo)
}
}
// MARK: - Firebase 取得登入使用者的资讯
func getFirebaseUserInfo() {
let currentUser = Auth.auth().currentUser
guard let user = currentUser else {
CustomFunc.customAlert(title: "无法取得使用者资料!", message: "", vc: self, actionHandler: nil)
return
}
let uid = user.uid
let email = user.email
CustomFunc.customAlert(title: "使用者资讯", message: "UID:\(uid)\nEmail:\(email!)", vc: self, actionHandler: nil)
}
}
然後如果要监听目前登入状况的话,Apple 提供了主动与被动这两种方法
下面这个是主动方法
// MARK: - 监听目前的 Apple ID 的登入状况
// 主动监听
func checkAppleIDCredentialState(userID: String) {
ASAuthorizationAppleIDProvider().getCredentialState(forUserID: userID) { credentialState, error in
switch credentialState {
case .authorized:
CustomFunc.customAlert(title: "使用者已授权!", message: "", vc: self, actionHandler: nil)
case .revoked:
CustomFunc.customAlert(title: "使用者凭证已被注销!", message: "请到\n「设定 → Apple ID → 密码与安全性 → 使用 Apple ID 的 App」\n将此 App 停止使用 Apple ID\n并再次使用 Apple ID 登入本 App!", vc: self, actionHandler: nil)
case .notFound:
CustomFunc.customAlert(title: "", message: "使用者尚未使用过 Apple ID 登入!", vc: self, actionHandler: nil)
case .transferred:
CustomFunc.customAlert(title: "请与开发者团队进行联系,以利进行使用者迁移!", message: "", vc: self, actionHandler: nil)
default:
break
}
}
}
下面这个是被动方法,无论是使用 Apple ID 登入或登出都会触发
但我在测试的时候,什麽都没发生,可能还需要去找一下问题
// 被动监听 (使用 Apple ID 登入或登出都会触发)
func observeAppleIDState() {
NotificationCenter.default.addObserver(forName: ASAuthorizationAppleIDProvider.credentialRevokedNotification, object: nil, queue: nil) { (notification: Notification) in
CustomFunc.customAlert(title: "使用者登入或登出", message: "", vc: self, actionHandler: nil)
}
}
本篇的范例程序码:Github
09-08-2021 本章内容 THE STATE HOOK Hook 可以做的事情 规则 使用us...
搜寻(Search) 就是从一群资料中找出符合某些条件的资料,当资料量非常庞大时,如何在短时间内有效...
连续 30 天不中断每天上传一支教学影片,教你如何用 React 加上 Firebase 打造社群...
铁人赛的最後一天,让我们先来简单的聊聊重构,这部分是笔者之前在看「大规模重构」这本书时整理的内容,目...
情境 有多个文件或图档利用checkbox勾选後,可以直接点选下载按钮。此时网站会将刚刚所选择的项目...