Sign In With Apple
苹果推出了 Sign in with Apple
功能(支持三方登陆的也必须支持苹果的)。
快到最后时限了还是搞一下吧,冲冲冲:
流程图:
一、配置
1、需要在苹果后台打开该选项,并且重新生成Profiles
配置文件,并安装到Xcode
2、服务端验证需要的文件,一个是私钥文件(.p8),一个是config.json
文件(这个后面说)
先搞私钥文件:
key->添加->重命名一下,选中sign in with Apple->单击Configure
按钮,然后选择你先前创建的Primary App ID
保存之后,Apple
将为你生成一个新的私钥,并让你仅下载一次,请确保你保存了此文件,
因为以后你将无法再次将其取回!
你下载的文件将以.p8
结尾,可以将其重命名为key.txt
以便在后续步骤中更轻松地使用
另外记一下你的keyID 后台会用到,就不用再来看了,如果两个APP维护,记得不要重名哦!
3、配置xcode11 如图点击 capability
二、代码处理(OC,需要swift的看最后的参考链接,不过逻辑都是一样的)
1、导入系统头文件#import <AuthenticationServices/AuthenticationServices.h>,添加Sign In with Apple登录按钮,设置ASAuthorizationAppleIDButton相关布局,并添加按钮点击响应事件
1.1 Sign In with Apple登录按钮有很多种样式可以修改style查看
1.2 这里我使用的通知获取苹果返回的验证信息(
viewDidLoad添加通知,dealloc移除通知
)
1.3 这里把获取苹果返回的验证信息工具类做成单例(具体代码见2)
注意: 苹果给的验证信息后台只能验证一次,并且是5分钟内,所以这里的登录操作不要重复,不然下次的重复登录会token验证失败
// sign in with apple // 使用系统提供的按钮,要注意不支持系统版本的处理 if (@available(iOS 13.0, *)) { vOrLine.hidden = YES; // Sign In With Apple Button ASAuthorizationAppleIDButton *appleIDBtn = [ASAuthorizationAppleIDButton buttonWithType:ASAuthorizationAppleIDButtonTypeDefault style:ASAuthorizationAppleIDButtonStyleWhiteOutline]; appleIDBtn.frame = CGRectMake(30, 5, v.width - 60, 40); [appleIDBtn addTarget:self action:@selector(didAppleIDBtnClicked) forControlEvents:UIControlEventTouchUpInside]; [v addSubview:appleIDBtn]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(signInApple:) name:NOTIFICATION_SignInApple object:nil]; }
// 使用系统提供的按钮调用处理授权的方法 - (void)didAppleIDBtnClicked{ // 封装Sign In with Apple [[DhSignInApple shareInstance] handleAuthorizationAppleIDButtonPress]; } -(void)signInApple : (NSNotification *)notification{ NSDictionary *userInfo = notification.userInfo; // 获取到数据 进行后台登录 }
- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; }
2、获取授权码并验证
2.1这里我把昵称也返给后台
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @class DhSignInApple; typedef void(^success) (NSDictionary *dic); typedef void(^failure) (NSString *errorMsg); @interface DhSignInApple : NSObject @property (nonatomic, copy)success successblock; // @property (nonatomic, copy)failure failureblock; // //实例化对象 + (instancetype)shareInstance; // 处理授权 - (void)handleAuthorizationAppleIDButtonPress; // 如果存在iCloud Keychain 凭证或者AppleID 凭证提示用户 - (void)perfomExistingAccountSetupFlows; @end NS_ASSUME_NONNULL_END
#import "DhSignInApple.h" #import"Helper.h" #import <AuthenticationServices/AuthenticationServices.h> @interface DhSignInApple ()<ASAuthorizationControllerDelegate,ASAuthorizationControllerPresentationContextProviding> @end @implementation DhSignInApple //实例化对象 + (instancetype)shareInstance { static DhSignInApple *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [DhSignInApple new]; }); return instance; } // 处理授权 - (void)handleAuthorizationAppleIDButtonPress{ 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{ NSLog(@"///已经认证过了/////"); 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登录"); } } #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 = authorization.credential; NSString *user = appleIDCredential.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. // 需要使用钥匙串的方式保存用户的唯一信息 // [YostarKeychain save:KEYCHAIN_IDENTIFIER(@"userIdentifier") data:user]; // nick_name NSString *name; if (familyName) { name = familyName; } if (givenName) { if (name.length > 0) { name = [NSString stringWithFormat:@"%@%@",name,givenName]; }else{ name = givenName; } } if(user.length > 0){ dispatch_async(dispatch_get_main_queue(), ^{ if (name.length > 0) { [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_SignInApple object:nil userInfo:@{@"ios_uid":user,@"nick_name":name,@"identity_token":identityTokenStr,@"authorization_code":authorizationCodeStr}]; }else{ [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_SignInApple object:nil userInfo:@{@"ios_uid":user,@"identity_token":identityTokenStr,@"authorization_code":authorizationCodeStr}]; } }); } }else if ([authorization.credential isKindOfClass:[ASPasswordCredential class]]){ // 这个获取的是iCloud记录的账号密码,需要输入框支持iOS 12 记录账号密码的新特性,如果不支持,可以忽略 // Sign in using an existing iCloud Keychain credential. // 用户登录使用现有的密码凭证 ASPasswordCredential *passwordCredential = authorization.credential; // 密码凭证对象的用户标识 用户的唯一标识 NSString *user = passwordCredential.user; // 密码凭证对象的密码 NSString *password = passwordCredential.password; }else{ NSLog(@"授权信息均不符"); } } // 授权失败的回调 - (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; } NSLog(@"%@", errorMsg); } // 告诉代理应该在哪个window 展示内容给用户 - (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0)){ NSLog(@"---window"); // 返回window return [UIApplication sharedApplication].windows.lastObject; } @end
在授权登录成功回调中,我们可以拿到以下几类数据
UserID:Unique, stable, team-scoped user ID
,苹果用户唯一标识符,该值在同一个开发者账号下的所有App
下是一样的,开发者可以用该唯一标识符与自己后台系统的账号体系绑定起来(这与国内的微信、QQ、微博
等第三方登录流程基本一致)
Verification data:Identity token, code
,验证数据,用于传给开发者后台服务器,然后开发者服务器再向苹果的身份验证服务端验证,本次授权登录请求数据的有效性和真实性,详见Sign In with Apple REST API
Account information:Name, verified email
,苹果用户信息,包括全名、邮箱等,
注意:如果玩家登录时拒绝提供真实的邮箱账号,苹果会生成虚拟的邮箱账号,而且记录过的苹果账号再次登录这些参数拿不到 。使用过授权的,下次就获取不到Name, email这些了。测试时可以进入设置密码与安全性--使用您Apple ID的APP--进行编辑删除操作,再次重新授权!
三、服务端
以上,从客户端拿到user
、 identityToken
、 authorizationCode
等相关信息。那么怎么来校验信息的正确性呢?
identityToken
做个校验,以某一次授权拿到的数据来举个例子。在上文的授权回调中拿到的identityToken
来验证,得到如下结果identityToken = "eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmZjYm94LmhpdmVjb25zdW1lciIsImV4cCI6MTU3NjIxNjc3NSwiaWF0IjoxNTc2MjE2MTc1LCJzdWIiOiIwMDE4NTcuNDBhODZjNDM4MzMwNDczNDgzZjk1YzcyMDA3MzY2YTYuMTAxNSIsImNfaGFzaCI6IjNCQlc1bWlaR3ZEX3RzNkNZdlUwR0EiLCJlbWFpbCI6Im45cHZ2Zms2cnNAcHJpdmF0ZXJlbGF5LmFwcGxlaWQuY29tIiwiZW1haWxfdmVyaWZpZWQiOiJ0cnVlIiwiaXNfcHJpdmF0ZV9lbWFpbCI6InRydWUiLCJhdXRoX3RpbWUiOjE1NzYyMTYxNzV9.Nksq3o1E8UxD4V7GmJB7ZrS0vSj_mm_ybdo7eiSbbAYNk6RnLuaRiJQYtI64mkZ-TqdeBgJmWt5bcSrW1gsWYk85YGeK79cIHaYO7nRIX1-e3_ociEJ3_dCECThrp-aMKZzq0yDz-xzbokZVsI4WmPcKlqhuE6ul2FBHwQrT3bTnxk_jB_4htqGjSW9u2cp2m-WbLrCgsorND3Z7w4KBICcEMqRnVbjTijO__-sgreXrFwDPu3LzccGQMr9cOugJorEe7gIEnACfOSF40YrsZ344SZfZ0VK9O8zOp6BoWw3yORDQiHkRjS0V9Tmi5SHQCGZ17kbjlrPUOQA0HgsVTQ"
https://appleid.apple.com/auth/token
该接口,client_id:
app
的 bundle identifier
client_secret: 需要我们自己生成,下文讲解生成方法
code: 即为手机端获取到的
authorizationCode
信息grant_type: 固定字符串
authorization_code
拿到上面4个参数之后,发起请求
client_secret
。下文代码为 Ruby
代码,确保已安装ruby
环境。创建一个secret_gen.rb
文件,把下面的代码拷贝进去。执行ruby secret_gen.rb
即可生成client_secret
require "jwt" key_file = "客户端步骤2中生成的私钥.p8文件的路径" team_id = "Team ID" client_id = "Bundle ID" key_id = "Key ID" 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
其他编程语言生成client_secret代码,参考这个吧,或许有用
https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
https://www.jianshu.com/p/2342260c95ff
team_id 如何获取 登录后在这里看
https://developer.apple.com/account/#/membership/
4、把生成的client_secret
代入步骤2中,得到的参数解释看这个文档,拿到id_token
其也是一个JWT
数据,回到JWT官网decode
出 payload
部分
比对服务端步骤1和步骤4图中的
aud
与sub
是否一致,若信息一致确定成功登录,其中aud
为app
的bundleID
。由于没有涉及到网页登录所以并没有集成iCloud KeyChain password
以上就是关于
Sign in with Apple
的相关内容和集成方法。参考:
https://www.jianshu.com/p/e1284bd8c72a
https://www.jianshu.com/p/4523c72c50bd