iOS 苹果以及谷歌三方登录
一.苹果三方登录
苹果三方登录分服务端授权码CODE登录方式和JWT登录方式,客户端代码部分相对简单
1.客户端(JWT方式)
其他总结文章:https://blog.csdn.net/cainiao1412/article/details/125820516
import AuthenticationServices
import JWTDecode//需要cocopods 导入,用于jwt解析
下面是编码部分
import AuthenticationServices
import JWTDecode
//点击按钮弹出授权弹窗
@IBAction func appleSignInButtonTapped(_ sender: UITapGestureRecognizer) {
        
        let request = ASAuthorizationAppleIDProvider().createRequest()
        request.requestedScopes = [.fullName, .email]
        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = self
        controller.presentationContextProvider = self
        controller.performRequests()
        
        
    }
//代理处理成功和失败
//MARK: ASAuthorizationControllerDelegate,ASAuthorizationControllerPresentationContextProviding
extension PCLoginViewController: ASAuthorizationControllerDelegate,ASAuthorizationControllerPresentationContextProviding {
    
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
            let userIdentifier = appleIDCredential.user
            print("User Identifier: \(userIdentifier)")
            let state = appleIDCredential.state
            print("State: \(state ?? "N/A")")
            let authorizedScopes = appleIDCredential.authorizedScopes
            print("Authorized Scopes: \(authorizedScopes)")
            let authorizationCode = appleIDCredential.authorizationCode
            print("Authorization Code: \(authorizationCode?.base64EncodedString() ?? "N/A")")
            let identityToken = appleIDCredential.identityToken
            let base64String = identityToken?.base64EncodedString() ?? "N/A"
            print("Identity Token: \(identityToken?.base64EncodedString() ?? "N/A")")
            
            let email = appleIDCredential.email
            print("Email: \(email ?? "N/A")")
            if let fullName = appleIDCredential.fullName {
                let formattedFullName = "\(fullName.givenName ?? "") \(fullName.familyName ?? "")"
                print("Full Name: \(formattedFullName)")
            }
            let realUserStatus = appleIDCredential.realUserStatus
            print("Real User Status: \(realUserStatus.rawValue)")
            // 可以在这里进一步处理用户信息
            // 解码Base64字符串
            if let data = Data(base64Encoded: base64String),
               let decodedString = String(data: data, encoding: .utf8) {
                print("解码结果:")
                parseIdentityToken(decodedString)
                print(decodedString)
            } else {
                print("解码失败")
            }
        }
    }
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        // 处理授权失败的情况
        var errorMsg: String?
         
         if let authorizationError = error as? ASAuthorizationError {
             switch authorizationError.code {
             case .canceled:
                 errorMsg = "用户取消了授权请求"
             case .failed:
                 errorMsg = "授权请求失败"
             case .invalidResponse:
                 errorMsg = "授权请求响应无效"
             case .notHandled:
                 errorMsg = "未能处理授权请求"
             case .unknown:
                 errorMsg = "授权请求失败未知原因"
             case .notInteractive:
                 errorMsg = "授权请求无需用户交互"
             @unknown default:
                 errorMsg = "未知错误"
             }
         } else {
             errorMsg = "未知错误"
         }
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+0.7) {
            PCToastView.shared.show(title: StringSafe(errorMsg))
        }
        
    }
    
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        //授权界面将会在当前视图控制器的窗口中弹出
        return self.view.window!
    }
  
  
    // 在你的授权回调中调用该方法
    // 例如:
    // parseIdentityToken(appleIDCredential.identityToken?.base64EncodedString())
    func parseIdentityToken(_ identityToken: String?) {
        guard let token = identityToken else {
            print("Identity Token is nil.")
            return
        }
        do {
            // 解码JWT令牌
            let jwt = try decode(jwt: token)
            // 输出解码后的头部和载荷
            print("Decoded Header:", jwt.header)
            print("Decoded Payload:", jwt.body)
//            // 获取特定信息,例如用户ID
            if let email = jwt.body["email"] as? String {
                print("Decoded email:", email)
            }
            
            // 在此可以执行其他操作,根据需要获取更多信息
        } catch {
            print("Error decoding Identity Token: \(error)")
        }
    }
    
}
Xcode记得打开 Sign in With App
appledevelop账号也需要把 Sign in With App 的选项打开
2.注意事项
(1)SDK只有首次授权才能获取到name和email参数
但是在identityToken解析后可以拿到email参数
(2)email 用户可以选择隐私模式,如果选择隐私模式邮箱会是另一个随机的隐私样式,有可能导致邮件发送到2个账户的情况。官方建议

(3) Sign in with Apple at Work & School的情况可能存在用户没有email的情况
这种情况是教育管理系统没有给学生分配邮箱的情况,此种情况App可以判断是否必须需要邮箱,如果App邮箱是必要字段可以给用户提示叫他们切换有邮箱的Apple的账号

2.授权码验证
官方文档
https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
注意事项

需要给后端 iss KeyID client_id
client_id 目前不确定是Bundle id 还是 appid 但是不能包含Teamid
目前认为 Bundle id 更加准确
官网原话
The identifier (App ID or Services ID) for your app. The identifier must not include your Team ID, to help prevent the possibility of exposing sensitive data to the end user
(4) 中国和印度可能存在用户没有email的情况因为他们支持手机号的Apple ID
https://support.apple.com/zh-cn/105034
(5) Sign in with Apple at Work & School的情况可能存在用户没有email的情况
注意
给隐藏邮件发信息需要在AppleDevelop后配置中继
上面配置服务器域名
下面配置发送的电子邮箱地址


(6) Server-to-Server Notification Endpoint
订阅服务端通知
Server to server notifications 是今年新推出一个特性。通过监听这些通知,可以让我们直接从服务端监视授权凭证状态更改等事件,并接收其他类型的事件。
首先,我们需要在苹果开发者网站上注册一个服务器端点(server endpoint),完成此注册后,就可以开始接收用户 Apple ID 状态发生变化事件了。
注:至于如何操作注册服务端通知,Session 里并没有细讲,也没有相关文档,可能目前苹果开发者网站还没更新,后续应该会补充吧,Apple Developer Forums 上也有人在问这个问题:https://developer.apple.com/forums/tags/wwdc20-10173
事件将通过由苹果签名过的 JSON Web Token 格式来传送,内容如下:

JSON 的内容包含了一些重要的信息,包括 app 的颁发者(issuer)和 BundleId 等,以及 event(事件)的具体内容。事件的类型有以下几种:
email-disabled:当用户决定停止从 private relay email 中接收邮件时,开发者会收到这个事件通知。
email-enabled:表示用户选择重新接收电子邮件。需要注意的是,只有当用户在授权时,选择“隐藏邮件地址”,使用一个专用的中转邮箱作为账户时,才会发送这两个事件。
consent-revoked:当用户决定停止在开发者的 app 中使用其 Apple ID 时,将向开发者发送“同意撤销”事件,此时 app 应将其视为用户已退出登录(sign out)。用户在 iOS 系统设置 -> Apple ID -> 密码与安全性 -> 使用 Apple ID 的 App,可以选择对某个 App 停止授权使用 Apple ID。
account-delete:当用户要求苹果删除其 Apple ID 时,将发送此事件。当收到此通知时,与用户关联的用户标识符将不再有效。
如你所见,通过监听这些通知,我们将能够以更好的方式直接从服务端对这四种不同的情况作出处理,以提升用户体验。
https://kangzubin.com/get-the-most-out-of-sign-in-with-apple/
二. 谷歌登录
官方文档:https://firebase.google.com/docs/auth/ios/google-signin?hl=zh-cn
https://developers.google.cn/identity/sign-in/ios/start-integrating?hl=zh-cn
1.通过cocopod安装GoogleSignIn
    pod 'GoogleSignIn'
2.配置google的clientID
首先在URL Types 配置
REVERSED_CLIENT_ID和clientID(官方文档说只配置REVERSED_CLIENT_ID)
但是我只配置单个后报错,所以都配置了

在网址白名单配置clientID

import GoogleSignIn
    @IBAction func googleSignInButtonTapped(_ sender: UITapGestureRecognizer) {
        
        let config = GIDConfiguration(clientID: "xxxxx-86193mc0uvg6lsg4hq32abvq38sg9iel.apps.googleusercontent.com")
        GIDSignIn.sharedInstance.configuration = config
        GIDSignIn.sharedInstance.signIn(withPresenting: self) { signInResult, error in
            
            if let error = error {
                print("error.localizedDescription1 = \(error.localizedDescription)")
            } else {
                if signInResult == nil {
                    print("signInResult is nil")
                } else {
                    signInResult?.user.refreshTokensIfNeeded(completion: { user, error in
                        if let error = error {
                            print("error.localizedDescription2 = \(error.localizedDescription)")
                        } else {
                            if user == nil {
                                print("user is nil")
                            } else {
                                //调取成功
                                print("user.userID: \(user?.userID ?? "")")
                                print("user.givenName: \(user?.profile?.givenName ?? "")")
                                print("user.familyName: \(user?.profile?.familyName ?? "")")
                                print("user.email: \(user?.profile?.email ?? "")")
                                print("user.idToken.tokenString: \(user?.idToken?.tokenString ?? "")")
                                
                                
                            }
                        }
                    })
                }
            }
            
        }
    }
注意
谷歌网页有一个填写隐私协议和用户协议的地方在这里
