iOS 13 Sign In with Apple
iOS 13
新增功能Sign In with Apple
,Sign In with Apple
是跨平台的,可以支持iOS
、macOS
、watchOS
、tvOS
、JS
。最近上架AppStore
因为没有Sign In with Apple
被拒了,本文主要内容为Sign In with Apple
在iOS
上的使用。
项目配置
-
在苹果开发者网站,在对应的
设置SignInWithAppleAppleId
下面开通Sign In With Apple
的权限。如下图
-
现在我只需要在
Signing & Capabilities
中添加Sign In With Apple
,Xcode
会自动帮你配置生成相关文件。其实上面第一步可以省略,这里可以自动生成。
代码集成
Sign In With Apple
流程
- 导入系统头文件
#import <AuthenticationServices/AuthenticationServices.h>
,创建相关Sign In with Apple
登录按钮,添加按钮点击响应事件 - 获取授权码
- 验证
- 执行自己的相关逻辑
注意⚠️:创建按钮可以自定义或者使用系统提供的按钮。
实际操作
-
导入头文件
#import <AuthenticationServices/AuthenticationServices.h>
-
创建对应UI
-(void)setUI{ // 使用系统提供的按钮,要注意不支持系统版本的处理 if (@available(iOS 13.0, *)) { // Sign In With Apple Button ASAuthorizationAppleIDButton *btn_apple = [ASAuthorizationAppleIDButton buttonWithType:ASAuthorizationAppleIDButtonTypeDefault style:ASAuthorizationAppleIDButtonStyleWhite]; btn_apple.frame = CGRectMake(50, 100, self.view.bounds.size.width - 100, 100); [btn_apple addTarget:self action:@selector(signWithApple:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:btn_apple]; } // 或者自己用UIButton实现按钮样式 UIButton *btn_custom = [UIButton buttonWithType:UIButtonTypeCustom]; btn_custom.frame = CGRectMake(50, 250, self.view.bounds.size.width - 100, 44); [btn_custom setTitle:@"Sign in with Apple" forState:UIControlStateNormal]; [btn_custom setTitleColor:[UIColor grayColor] forState:UIControlStateNormal]; [btn_custom addTarget:self action:@selector(signWithApple:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:btn_custom]; } -(void)signWithApple:(UIButton *)sender{ [[SignWithAppleManger sharedManager]SignInWithAppleWithBlock:^(id _Nonnull data, BOOL success, BOOL showError) { NSLog(@"123"); }]; }
注意⚠️:封装Sign In with Apple
登录工具类,使用这个类时要把类对象设置为全局变量,或者直接把这个工具类做成单例,如果使用局部变量,和IAP支付工具类一样,会导致苹果回调不会执行。已经使用Sign In with Apple
登录过App
的用户如果设备中存在iCloud Keychain
凭证或者AppleID
凭证,提示用户直接使用TouchID
或FaceID
登录即可。
-(void)ExistingSignInWithAppleWithBlock:(SignBackInfoBlock)block{
self.block = block;
[self observeAppleSignInState];
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];
NSMutableArray <ASAuthorizationRequest *>* requestArr = [NSMutableArray arrayWithCapacity:2];
if (appleIdRequest) {
[requestArr addObject:appleIdRequest];
}
if (passwordRequest) {
[requestArr addObject:passwordRequest];
}
// ASAuthorizationRequest:对于不同种类授权请求的基类
NSArray <ASAuthorizationRequest *>* requests = [requestArr copy];
// 由ASAuthorizationAppleIDProvider创建的授权请求 管理授权请求的控制器
ASAuthorizationController *authorizationController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:requests];
// 设置授权控制器通知授权请求的成功与失败的代理
authorizationController.delegate = self;
// 设置提供 展示上下文的代理,在这个上下文中 系统可以展示授权界面给用户
authorizationController.presentationContextProvider = self;
// 在控制器初始化期间启动授权流
[authorizationController performRequests];
}else{
// 处理不支持系统版本
if (self.block) {
self.block(@"该系统版本不可用Apple登录", NO, NO);
}
}
}
-
获取授权码
获取授权码需要在代码中实现两个代理回调ASAuthorizationControllerDelegate
、ASAuthorizationControllerPresentationContextProviding
分别用于处理授权登录成功和失败。#pragma mark - delegate //@optional 授权成功地回调 - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)){ // NSLog(@"授权完成:::%@", authorization.credential); // NSLog(@"%s", __FUNCTION__); // NSLog(@"%@", controller); // NSLog(@"%@", authorization); if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) { // 用户登录使用ASAuthorizationAppleIDCredential ASAuthorizationAppleIDCredential *appleIDCredential = (ASAuthorizationAppleIDCredential *)authorization.credential; NSString *user = appleIDCredential.user; // 使用钥匙串的方式保存用户的唯一信息 [SAMKeychain setPassword:user forService:[NSBundle mainBundle].bundleIdentifier account:ShareCurrentIdentifier]; // 使用过授权的,可能获取不到以下三个参数 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]; if (self.block) { self.block(user, YES, NO); } }else if ([authorization.credential isKindOfClass:[ASPasswordCredential class]]){ // 这个获取的是iCloud记录的账号密码,需要输入框支持iOS 12 记录账号密码的新特性,如果不支持,可以忽略 // Sign in using an existing iCloud Keychain credential. // 用户登录使用现有的密码凭证 ASPasswordCredential *passwordCredential = (ASPasswordCredential *)authorization.credential; // 密码凭证对象的用户标识 用户的唯一标识 NSString *user = passwordCredential.user; // 密码凭证对象的密码 NSString *password = passwordCredential.password; }else{ if (self.block) { self.block(@"授权信息均不符", NO, NO); } } } //MARK:授权失败的回调 - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)){ // Handle 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; } if (self.block) { self.block(errorMsg, NO, YES); } } //MARK ASAuthorizationControllerPresentationContextProviding //告诉代理应该在哪个window 展示内容给用户 - (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0)){ // 返回window return [UIApplication sharedApplication].windows.lastObject; }
注意⚠️:完成上述后一般会返回需要的数据,如下
-
UserID
:Unique
,stable
,team-scoped user ID
,苹果用户唯一标识符,该值在同一个开发者账号下的所有App下是一样的,开发者可以用该唯一标识符与自己后台系统的账号体系绑定起来。 -
Verification data
:Identity token
,code
,验证数据,用于传给开发者后台服务器,然后开发者服务器再向苹果的身份验证服务端验证,本次授权登录请求数据的有效性和真实性 -
Account information
:Name
,verified email
,苹果用户信息,包括全名、邮箱等,注意:如果玩家登录时拒绝提供真实的邮箱账号,苹果会生成虚拟的邮箱账号,而且记录过的苹果账号再次登录这些参数拿不到。 - 验证,关于验证的这一步,需要传递授权码给自己的服务端,自己的服务端调用苹果
API
去校验授权码Generate and validate tokens
。
注意⚠️:验证一般是app端获取到信息,然后把authorizationCode
传给后台,验证地址,苹果返回id_token
,与客户端获取的identityToken值一样,格式如下
{
"access_token": "一个token",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "一个token",
"id_token": "结果是JWT,字符串形式,identityToken"
}
授权code
是有时效性的,且使用一次即失效
服务器拿到相应结果后,其中id_token
是JWT
数据,解码id_token
,得到如下内容
{
"iss":"https://appleid.apple.com",
"aud":"这个是你的app的bundle identifier",
"exp":1567482337,
"iat":1567481737,
"sub":"这个字段和客户端获取的user字段是完全一样的",
"c_hash":"8KDzfalU5kygg5zxXiX7dA",
"auth_time":1567481737
}
其中aud
与你app
的bundleID
一致,sub
就是授权用户的唯一标识,与手机端获得的user
一致,服务器端通过对比sub
字段信息是否与手机端上传的user
信息一致来确定是否成功登录
该token
的有效期是10
分钟。