Apple登录与实际业务结合的一点开发总结

1,895 阅读6分钟

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

前言

最近公司的一个旧项目,非要做第三方登录,说是为了让用户在登录时体验更加友好。

我觉得吧,一个App了做了某个阶段,然后再考虑引入第三方登录,简直就是脑残。

目前App已经是通过手机号作为登录标识符,而且在后台的数据库中,用户的uid和手机号已经做了绑定,基本上可以认为手机号已经作为了主键使用。

现阶段又加入第三方登录,在用户首次第三方登录成功后,还是需要用户进行手机号的关联,我觉得有点脱裤子放屁的感觉。

大伙对这块怎么看,可以一起讨论,这个只是我比较激进的观点,因为害我要写功能,不能开心的在掘金上面摸鱼了。

当然说到加入第三方登录,那么根据Apple的规定,Apple登录也必须加上,否则无法上架。

所以才有了这篇文章,针对Apple登录的一点开发总结,虽然都是2年前的知识点,但我也算是旧瓶新酒,结合自己开发过程中App自身的业务情况,做了总结与思考,权当自己做笔记吧。

大伙注意看代码块中的注释喔!!!

Apple登录

开启该功能

我们自己的App要能够使用这个功能,需要在App中开启这个功能:

image.png

然后在developer开发网站中,Identifiers→对应App的BoundleID中也开启这个功能:

image.png

苹果官方Demo学习

我在编写这个功能的时候,非常仔细的研究了苹果官方给出的Demo代码。

有很多时候,当我们对某个新功能无从下手的时候,查看和学习官方代码是一个非常不错的例子。

Apple登录的逻辑处理,这一步非常的简单,就是起一个服务,设置好代理回调,请求走起!

@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 {
        /// 注意,第一次进行Apple登录,优先使用生物识别,指纹或者faceId如果此时生物识别没有通过,会提示你使用密码进行Apple登录,如果此时密码通过了,还是会走到这个case let appleIDCredential as ASAuthorizationAppleIDCredential中来,而一旦第一次生物识别过了,后生物识别没有通过,而依旧使用密码进行Apple登录,还是会走这里
        case let appleIDCredential as ASAuthorizationAppleIDCredential:

            /// 系统生成的user,相当于用户名
            let userIdentifier = appleIDCredential.user
            /// 将这个用户名保存到Keychain中
            self.saveUserInKeychain(userIdentifier)
            
            /// 这两个有且仅有在第一次使用Apple登录的时候才能获取到,第二次之后就拿不到这些数据了
            let fullName = appleIDCredential.fullName
            let email = appleIDCredential.email
            
            /// 这个jwt字符串非常重要,需要传给App后台,App后台调用Apple后台,去验证账号的有效性
            if let identityToken = appleIDCredential.identityToken {
                let jwt = String(data: identityToken, encoding: .utf8)
                print(jwt)
            }
        /// 给那种没有生物识别功能但是又使用Apple登录的设备,看来应该不是iPhone\iPad系列的产品,旧Mac\TV\Watch
        case let passwordCredential as ASPasswordCredential:
        
            // Sign in using an existing iCloud Keychain credential.
            let username = passwordCredential.user
            let password = passwordCredential.password
        default:
            break
        }
    }
}

总体而言,这个Apple登录成功的回调中有重要的信息,在这里面我们需要做以下几个操作:

  • 获取appleIDCredential.user

  • 将appleIDCredential.user保存在Keychain中,为什么要保存,我们后面会说到

  • 将appleIDCredential.identityToken转为jwt字符串,便于传递给App后台,然后App后台与Apple后台交互,做校验(虽然我们移动端不用关系后台与后台的交互问题,但是了解一点没坏处)

Apple登录的授权取消与App此时需要做的事情

IMG_3611DBC24607-1.jpeg

这里我们又细分成为两种情况:

  • App通过Apple登录挂在后台,而此时去系统设置页面,删除了Apple登录在此App中的授权,此时App需要做对应的操作,我们可以通过监听通知的方式获知这个事情

    extension AppDelegate {
    
        func observeAppleSignInState() {
            NotificationCenter.default.addObserver(self, selector: #selector(handleSignInWithAppleStateChanged(notification:)), name: ASAuthorizationAppleIDProvider.credentialRevokedNotification, object: nil)
        }
    
        @objc
        func handleSignInWithAppleStateChanged(notification: NSNotification) {
            print(notification.userInfo)
            /// 删除在Keychain中保存当前Apple账号生成的appleIDCredential.user
            /// 做App登出等操作
        }
    
    }
    
  • App被杀死,而此时去系统设置页面,删除了Apple登录在此App中的授权,下一次点击进入App的时候,,我们可以通过系统给出的这个回调处理一些业务,同时需要注意,这里我们是通过从Keychain中获取之前保存的userIdentifier,才能获得其状态

    extension AppDelegate {
    
        func getCredentialState() {
            let appleIDProvider = ASAuthorizationAppleIDProvider()
            
            /// 注意这里的入参是KeychainItem.currentUserIdentifier,更详细的可以看官方Demo,这里注意是研究逻辑而不是代码实现
            appleIDProvider.getCredentialState(forUserID: KeychainItem.currentUserIdentifier) { (credentialState, error) in
    
                switch credentialState {
                case .authorized:
                    // The Apple ID credential is valid.
                    break
                case .revoked, .notFound:
                    // The Apple ID credential is either revoked or was not found, so show the sign-in UI.
                    /// 删除在Keychain中保存当前Apple账号生成的appleIDCredential.user
                    /// 如果App有自动登录功能,应该阻止,并删除相关对应账户的信息
                    break
                default:
                    break
                }
            }
        }
    }
    
  • 把以上的两种情况的方法都添加到func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool中即可:

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        getCredentialState()
        observeAppleSignInState()
        return true
    }
    

思考

大家需要注意这样一种业务情况:

App先使用Apple登录成功了,然后退出登录,再用QQ登录成功了,此时退出登录,然后我们取消Apple登录在此App中的授权,再次启动次App,根据上面写的逻辑,必然会走到处理Apple登录状态改变的逻辑中。

那么我们需要思考的问题也来了:

  • 多种第三方登录业务混合的时候,如果对应Apple登录取消授权,是一个我们需要注意与考虑的地方。我在此次的App改造中,因为自身App的业务情况吃过亏,大家也需要注意,保存上一次成功第三方登录的方式有用处。

  • 为何Apple登录的appleIDCredential.user要保存到Keychain中,保存到App沙盒不行吗?

  • 为何在知道Apple登录授权取消时,我会删除在Keychain中保存的appleIDCredential.user?

参考文档

Implementing User Authentication with Sign in with Apple

jwt.io

苹果登录Sign In With Apple

iOS 13 苹果账号登陆与后台验证相关

AppleID 授权登陆App(Java后端验证)

总结

其实整体而言,Apple登录并不算特别复杂,它其实和你使用其他第三方登录,例如QQ、微信都是一个原理。

image.png

把这个流程图中的Apple后台换成QQ后台或者微信后台,其思路也一样的

  • App先通过QQ SDK/微信 SDK获取登录标识符

  • App将登录标识符回传给App后台

  • App后台再将登录标识符给QQ后台/微信后台做校验

  • QQ后台/微信后台回调给App后台,App后台回传给App

这里我结合了业务情况,留下了思考和我对第三方登录的理解,或许在你也有集成Apple登录的时候会得到一点启发。