owned this note
owned this note
Published
Linked with GitHub
###### tags: `第13屆IT邦鐵人賽文章`
# 【在 iOS 開發路上的大小事-Day22】透過 Firebase 來管理使用者 (Sign in with Apple 篇) Part2
# 溫馨回顧
在前一篇我們有簡單介紹了 Sign in with Apple 是什麼,有哪些使用限制
以及完成了 Sign in with Apple 的前置作業
像是在 Xcode 內新增 Sign in with Apple 的 Capability、Firebase Auth 內啟用 Apple 登入
這篇我們要來將功能實作出來~
# 開始實作~
首先先引入 Firebase Auth、AuthenticationServices、CryptoKit 這三個
```swift=
import FirebaseAuth // 用來與 Firebase Auth 進行串接用的
import AuthenticationServices // Sign in with Apple 的主體框架
import CryptoKit // 用來產生隨機字串 (Nonce) 的
```
接著我們來建立一個 Sign in with Apple 的按鈕,並且可以根據系統模式來變更顯示顏色
如果是淺色模式就顯示黑色的,深色模式就顯示白色的
按鈕樣式可以參考 Apple 官方文件:[Buttons](https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/buttons/)
或者是可以透過 Apple 官方提供的線上設計來模擬:[Sign in with Apple Button](https://appleid.apple.com/signinwithapple/button)
```swift=
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 // 淺色模式就顯示黑色的按鈕,深色模式就顯示白色的按鈕
}
```
接下來是 **請求登入的動作**
```swift=
// 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 (重送攻擊) 是很重要的
```swift=+
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 的環節
這個環節是用來進行登入成功與登入失敗的邏輯處理
```swift=
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 的環節
這個環節是用來告訴說要在哪個畫面上呈現授權畫面
```swift=
// MARK: - ASAuthorizationControllerPresentationContextProviding
// 在畫面上顯示授權畫面
extension SignInWithAppleVC: ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return view.window!
}
}
```
接下來是透過 Credential 與 Firebase Auth 進行串接
```swift=
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 提供了主動與被動這兩種方法
下面這個是主動方法
```swift=
// 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 登入或登出都會觸發
但我在測試的時候,什麼都沒發生,可能還需要去找一下問題
```swift=+
// 被動監聽 (使用 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)
}
}
```
# 成果
{%youtube khxzsBoExPs %}
本篇的範例程式碼:[Github](https://github.com/leoho0722/IT2021/blob/main/Day15~Day22%E3%80%81Day24~Day28/CocoaPodsDemo/CocoaPodsDemo/FirebaseVC/SignInWithAppleVC/SignInWithAppleVC.swift)
# 參考資料
1. Sign In with Apple(Apple 登入)-[法蘭克的iOS世界](https://franksios.medium.com/ios-sign-in-with-apple-apple-%E7%99%BB%E5%85%A5-9b175596b69)
2. 如何整合 Sign in with Apple 到自己的 iOS App 上 (iOS & Backend)-[兔子](https://medium.com/@tuzaiz/%E5%A6%82%E4%BD%95%E6%95%B4%E5%90%88-sign-in-with-apple-%E5%88%B0%E8%87%AA%E5%B7%B1%E7%9A%84-ios-app-%E4%B8%8A-ios-backend-e64d9de15410)
3. Authenticate Using Apple on iOS-[Firebase Auth 官方文件](https://firebase.google.com/docs/auth/ios/apple#sign_in_with_apple_and_authenticate_with_firebase)