sign in with Apple 接入

1.项目配置

  • 需要配置具备sign in with Apple功能的profile文件
  • xcode项目的 signing and capabilities中需要新增sign in with apple模块

2.代码接入

1. 创建登录按钮

       // 使用系统提供的按钮,要注意不支持系统版本的处理
   if (@available(iOS 13.0, *)) {
       ASAuthorizationAppleIDButton *appleIDBtn = [ASAuthorizationAppleIDButton buttonWithType:ASAuthorizationAppleIDButtonTypeDefault style:ASAuthorizationAppleIDButtonStyleWhite];
       appleIDBtn.frame = CGRectMake(30, self.view.bounds.size.height - 180,   self.view.bounds.size.width - 60, 100);
       [appleIDBtn addTarget:self action:@selector(didAppleIDBtnClicked)       forControlEvents:UIControlEventTouchUpInside];
   }

这里的sign in with apple按钮有两种文本,三种样式:

   typedef NS_ENUM(NSInteger, ASAuthorizationAppleIDButtonType) {
   ASAuthorizationAppleIDButtonTypeSignIn,
   ASAuthorizationAppleIDButtonTypeContinue,

   ASAuthorizationAppleIDButtonTypeSignUp API_AVAILABLE(ios(13.2),     macos(10.15.1), tvos(13.1)) API_UNAVAILABLE(watchos),

   ASAuthorizationAppleIDButtonTypeDefault =   ASAuthorizationAppleIDButtonTypeSignIn,
   } NS_SWIFT_NAME(ASAuthorizationAppleIDButton.ButtonType)    API_AVAILABLE(ios(13.0), macos(10.15), tvos(13.0))  API_UNAVAILABLE(watchos);


   typedef NS_ENUM(NSInteger, ASAuthorizationAppleIDButtonStyle) {
   ASAuthorizationAppleIDButtonStyleWhite,
   ASAuthorizationAppleIDButtonStyleWhiteOutline,
   ASAuthorizationAppleIDButtonStyleBlack,
   } NS_SWIFT_NAME(ASAuthorizationAppleIDButton.Style)     API_AVAILABLE(ios(13.0), macos(10.15), tvos(13.0))  API_UNAVAILABLE(watchos);
   

2. 监听按钮点击事件,发送登录请求

if (@available(iOS 13.0, *)) {
        // 基于用户的Apple ID授权用户,生成用户授权请求的一种机制
        ASAuthorizationAppleIDProvider *appleIDProvider = [[ASAuthorizationAppleIDProvider alloc] init];
        // 创建新的AppleID 授权请求
        ASAuthorizationAppleIDRequest *appleIDRequest = [appleIDProvider createRequest];
        // 在用户授权期间请求的联系信息
        appleIDRequest.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
        // 由ASAuthorizationAppleIDProvider创建的授权请求 管理授权请求的控制器
        ASAuthorizationController *authorizationController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[appleIDRequest]];
        // 设置授权控制器通知授权请求的成功与失败的代理
        authorizationController.delegate = self;
        // 设置提供 展示上下文的代理,在这个上下文中 系统可以展示授权界面给用户
        authorizationController.presentationContextProvider = self;
        // 开始登录请求
        [authorizationController performRequests];
    }else{
        // 处理不支持系统版本
        NSLog(@"该系统版本不可用Apple登录");
    }
// 如果存在iCloud Keychain 凭证或者AppleID 凭证提示用户
- (void)perfomExistingAccountSetupFlows{
    
    if (@available(iOS 13.0, *)) {
        // 基于用户的Apple ID授权用户,生成用户授权请求的一种机制
        ASAuthorizationAppleIDProvider *appleIDProvider = [[ASAuthorizationAppleIDProvider alloc] init];
        // 授权请求AppleID
        ASAuthorizationAppleIDRequest *appleIDRequest = [appleIDProvider createRequest];
        // 为了执行钥匙串凭证分享生成请求的一种机制
        ASAuthorizationPasswordProvider *passwordProvider = [[ASAuthorizationPasswordProvider alloc] init];
        ASAuthorizationPasswordRequest *passwordRequest = [passwordProvider createRequest];
        // 由ASAuthorizationAppleIDProvider创建的授权请求 管理授权请求的控制器
        ASAuthorizationController *authorizationController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[appleIDRequest, passwordRequest]];
        // 设置授权控制器通知授权请求的成功与失败的代理
        authorizationController.delegate = self;
        // 设置提供 展示上下文的代理,在这个上下文中 系统可以展示授权界面给用户
        authorizationController.presentationContextProvider = self;
        // 在控制器初始化期间启动授权流
        [authorizationController performRequests];
    }else{
        // 处理不支持系统版本
        NSLog(@"该系统版本不可用Apple登录");
    }
}

3. 通过代理方法监听回调结果

  • 授权成功
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)){
 
 if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
     // 用户登录使用ASAuthorizationAppleIDCredential
     ASAuthorizationAppleIDCredential *appleIDCredential = authorization.credential;
     NSString *user = appleIDCredential.user;
     
     if (user) {
         [YostarKeychain save:KEYCHAIN_IDENTIFIER(@"userIdentifier") data:user];
     }
     // 使用过授权的,可能获取不到以下三个参数
     NSString *familyName = appleIDCredential.fullName.familyName;
     NSString *givenName = appleIDCredential.fullName.givenName;
     NSString *email = appleIDCredential.email;
     
     NSData *identityToken = appleIDCredential.identityToken;
     NSData *authorizationCode = appleIDCredential.authorizationCode;
     
     // 服务器验证需要使用的参数
     NSString *identityTokenStr = [[NSString alloc] initWithData:identityToken encoding:NSUTF8StringEncoding];
     NSString *authorizationCodeStr = [[NSString alloc] initWithData:authorizationCode encoding:NSUTF8StringEncoding];
     NSLog(@"%@\n\n%@", identityTokenStr, authorizationCodeStr);
     
     // Create an account in your system.
     // For the purpose of this demo app, store the userIdentifier in the keychain.
     
 }
}

sign in with Apple授权成功后,可以拿到以下数据:

 > 1.userId :用户的唯一标识符,一个apple ID对应一个唯一的userId,通过这个id可以  在后台用作数据绑定

> 2.Verification data:后台用来用作身份验证的数据,具体验证流程参考:https://www.yuque.com/zhanglong/bb0s5d/cxbh7n

> 3.用户名和邮箱数据
  • 授权失败
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)){
 // Handle error.
 NSLog(@"Handle error:%@", error);
 NSString *errorMsg = nil;
 switch (error.code) {
     case ASAuthorizationErrorCanceled:
         errorMsg = @"用户取消了授权请求";
         break;
     case ASAuthorizationErrorFailed:
         errorMsg = @"授权请求失败";
         break;
     case ASAuthorizationErrorInvalidResponse:
         errorMsg = @"授权请求响应无效";
         break;
     case ASAuthorizationErrorNotHandled:
         errorMsg = @"未能处理授权请求";
         break;
     case ASAuthorizationErrorUnknown:
         errorMsg = @"授权请求失败未知原因";
         break;
         
     default:
         break;
 }
}

4.当用户终止app使用sign in with apple功能或者在设置中注销apple ID时,需要做退出登录处理,此时需要对此行为做监听,具体是在进入应用和后台返回前台时,需要用到userID去查询此时账户的登录状态,具体代码如下:

- (void)appleLoginStatueChange{
    
    if (@available(iOS 13.0, *)) {
            // A mechanism for generating requests to authenticate users based on their Apple ID.
            // 基于用户的Apple ID 生成授权用户请求的机制
            ASAuthorizationAppleIDProvider *appleIDProvider = [ASAuthorizationAppleIDProvider new];
            // 注意 存储用户标识信息需要使用钥匙串来存储 这里笔者简单期间 使用NSUserDefaults 做的简单示例
            NSString *userIdentifier = [YostarKeychain load:KEYCHAIN_IDENTIFIER(@"userIdentifier")];
            NSLog(@"observeAuthticationState----:%@",userIdentifier);
            if (userIdentifier) {
                NSString* __block errorMsg = nil;
                //Returns the credential state for the given user in a completion handler.
                // 在回调中返回用户的授权状态
                [appleIDProvider getCredentialStateForUserID:userIdentifier completion:^(ASAuthorizationAppleIDProviderCredentialState credentialState, NSError * _Nullable error) {
                    switch (credentialState) {
                            // 苹果证书的授权状态
                        case ASAuthorizationAppleIDProviderCredentialRevoked:
          // 苹果授权凭证失效 -> 此时是用户在设置中终止了此App的sign in with Apple功能
                            errorMsg = @"苹果授权凭证失效";
                            // 需要重新登录
                            break;
                        case ASAuthorizationAppleIDProviderCredentialAuthorized:
                            // 苹果授权凭证状态良好
                            errorMsg = @"苹果授权凭证状态良好";
                            break;
                        case ASAuthorizationAppleIDProviderCredentialNotFound:
                            // 未发现苹果授权凭证 -> 此时是注销apple ID的时候
                            // 需要重新登录
                            errorMsg = @"未发现苹果授权凭证";
                            break;
                            // 可以引导用户重新登录
                        case ASAuthorizationAppleIDProviderCredentialTransferred:
                            errorMsg = @"苹果授权信息变动";
                            break;
                    }
                    dispatch_async(dispatch_get_main_queue(), ^{
                        NSLog(@"SignInWithApple授权状态变化情况");
                        NSLog(@"%@", errorMsg);
                    });
            }];
                
        }
    }
}

5.后台验证

**主要流程是客户端拿到userId, identityToken, authorizationCode后传给后台,后台拼接参数调用接口:https://appleid.apple.com/auth/token去苹果服务器请求认证。

1. 参数的生成

client_id: 为app的 bundle identifier

code:为手机端获取到的 authorizationCode 信息

grant_type:传入固定字符串 authorization_code

client_secret:需要自己我们通过私钥生成

2.client_secret的生成

注意,私钥只能下载一次
require "jwt"

key_file = "Path to the private key"
team_id = "Your Team ID"
client_id = "Your App Bundle ID"
key_id = "The Key ID of the private key"
validity_period = 180 # In days. Max 180 (6 months) according to Apple docs.

private_key = OpenSSL::PKey::EC.new IO.read key_file

token = JWT.encode(
  {
    iss: team_id,
    iat: Time.now.to_i,
    exp: Time.now.to_i + 86400 * validity_period,
    aud: "https://appleid.apple.com",
    sub: client_id
  },
  private_key,
  "ES256",
  header_fields=
  {
    kid: key_id 
  }
)
puts token

3.创建文件 secret_gen.rb ,把上面代码粘贴进去,执行 ruby secret_gen.rb 即可生成 client_secret
代码中这个 key_file 需要指定刚才下载的文件的地址

https://appleid.apple.com/auth/token

{

"access_token": "一个token,此处省略",

"token_type": "Bearer",

"expires_in": 3600,

"refresh_token": "一个token,此处省略",

"id_token": "结果是JWT,字符串形式,此处省略"

}
服务器拿到相应结果,其中 id_token 也是 JWT 数据,decode 出 payload 部分如下

{
  "iss": "https://appleid.apple.com",
  "aud": "这个对应app的bundleid",
  "exp": 1567494694,
  "iat": 1567494094,
  "sub": "这个字段和手机端获取的user信息相同",
  "at_hash": "nRYP2wGXBGT0bIYWibx4Yg",
  "auth_time": 1567494094
}

其中 aud 部分与你的app的bundleID一致, sub 部分即与手机端获得的 user 一致,服务器端通过对比 sub 字段信息是否与手机端上传的 user 信息一致来确定是否成功登录.

更详细的接入信息请参考:

facebook登录接入(SDK6.2.0)

1.首先进入facebook开发者官方网站

并且成为其开发者。

2.创建应用,获取应用编号

3.进入facebook集成登录模块

  • 1.选择需要应用
  • 2.选择集成方式,此处建议使用cocoapods方式集成,官方提供下载的SDK缺少Bolts库,会导致集成错误
  • 3.在 Facebook 注册和配置您的应用,配置bundle ID
  • 4.配置工程项目,在xocde项目的info.plist中配置:
<key>CFBundleURLTypes</key> 
<array> 
<dict> 
<key>CFBundleURLSchemes</key> 
<array> <string>fb2516056732057358</string> </array> 
</dict> 
</array> 
<key>FacebookAppID</key> 
<string>2516056732057358</string> 
<key>FacebookDisplayName</key> 
<string>changeDemo</string>
 <key>LSApplicationQueriesSchemes</key> 
 <array>
 <string>fbapi</string> 
 <string>fbapi20130214</string>
 <string>fbapi20130410</string>
 <string>fbapi20130702</string> 
 <string>fbapi20131010</string> 
 <string>fbapi20131219</string> 
 <string>fbapi20140410</string> 
 <string>fbapi20140116</string> 
 <string>fbapi20150313</string> 
 <string>fbapi20150629</string> 
 <string>fbapi20160328</string>
 <string>fbauth</string> 
 <string>fb-messenger-share-api</string>
 <string>fbauth2</string> 
 <string>fbshareextension</string>
 </array>
  • 5.在appdelegate.m文件中导入:#import < FBSDKCoreKit/FBSDKCoreKit.h>,此时不考虑SceneDelegate存在的情况.
 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 
    [[ApplicationDelegate sharedInstance] application:application didFinishLaunchingWithOptions:launchOptions]; 
    // Add any custom logic here. 
    return YES;
 } 
 
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
     BOOL handled = [[ApplicationDelegate sharedInstance] application:application openURL:url sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey] annotation:options[UIApplicationOpenURLOptionsAnnotationKey] ]; 
     // Add any custom logic here. 
     return handled;
}

如果应用中存在SceneDelegate,那么还需配置一下代码:

- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts { 
    UIOpenURLContext *openURLContext = URLContexts.allObjects.firstObject; 
    if (openURLContext) { 
         [[ApplicationDelegate sharedInstance] application:UIApplication.sharedApplication  openURL:openURLContext.URL sourceApplication:openURLContext.options.sourceApplication annotation:openURLContext.options.annotation]; 
    } 
    // Add any custom logic here. 
}
    
 FBSDKLoginButton *loginButton = [[FBSDKLoginButton alloc] init];
     // Optional: Place the button in the center of your view.
  loginButton.center = self.view.center;
 loginButton.permissions = @[@"public_profile", @"email"];
 loginButton.delegate = self;
     [self.view addSubview:loginButton];
 通过FBSDKLoginButtonDelegate代理监听登录的状态:
- (void)loginButton:(FBSDKLoginButton *)loginButton
didCompleteWithResult:(nullable FBSDKLoginManagerLoginResult *)result
          error:(nullable NSError *)error{
     if (result.token) {
            NSLog(@"登录成功,userID = %@",result.token.userID);
    }else{
            NSLog(@"登录失败");
        }
}
- (void)loginButtonDidLogOut:(FBSDKLoginButton *)loginButton{
        NSLog(@"---退出登录");
}
[self.loginManager logInWithPermissions:@[@"public_profile", @"email"]  fromViewController:self handler:^(FBSDKLoginManagerLoginResult * _Nullable result,  NSError * _Nullable error) {
    if (result.token) {
            NSLog(@"登录成功,userID = %@",result.token.userID);
    }else{
             NSLog(@"登录失败");
    }
}];
    // 退出登录
    [self.loginManager logOut];
if ([FBSDKAccessToken currentAccessToken]) {
 // User is logged in, do work such as go to next view controller. 
}

facebook登录成功,最终会获取到FBSDKAccessToken类型的模型,如下:

@property (class, nonatomic, copy, nullable) FBSDKAccessToken *currentAccessToken;

@property (nonatomic, copy, readonly) NSDate *expirationDate;

@property (nonatomic, copy, readonly) NSDate *refreshDate;

// 身份验证的令牌
@property (nonatomic, copy, readonly) NSString *tokenString;

// 用户唯一id
@property (nonatomic, copy, readonly) NSString *userID;

这里我们需要传给后台的就是,userID和tokenString

经测试发现最新的facebook不支持facebook App授权登录,仅支持应用内跳转到网页登录