Sign In With Apple(一)

avatar
奇舞团移动端团队 @奇舞团

级别: ★☆☆☆☆
标签:「iOS 13」「双重因子验证」「Sign In With Apple」
作者: WYW
审校: QiShare团队


前言 笔者最近了解了iOS13 新增的功能之Sign In With Apple。会输出2篇文章,给大家分享一下。这是第一篇文章,主要的内容为Sign In With Apple及相关名词的介绍,及在iOS上的基础使用。

一、Sign In With Apple 简介

苹果官方 是这么介绍的Sign In With Apple

The fast, easy way to sign in to apps and websites.

Sign In With Apple 是一种在app 和网站上快速、容易登录的方式。

Sign In with Apple makes it easy for users to sign in to your apps and websites using their Apple ID. Instead of filling out forms, verifying email addresses, and choosing new passwords, they can use Sign In with Apple to set up an account and start using your app right away. All accounts are protected with two-factor authentication for superior security, and Apple will not track users’ activity in your app or website.

对于用户来说,Sign In With Apple 使他们可以使用Apple ID容易地登录apps和网站。而不需要填写表单,验证邮件,选择新密码。用户可以使用Sign In With Apple 创建新用户并立即可以开始使用你的app。为了提高安全性,双重因子验证保护了帐号的安全性。而且Apple 不会跟踪用户在app 和网站的行为信息。

下边笔者先简单介绍一下双重因子验证及开发Sign In With Apple 的注意事项。

1. 双重因子验证

这里笔者举个例子说明一下双重因子验证。比如:

前提:我们有2个苹果设备A,B。我们已经在设备A上登录过了苹果帐号QiShare,B 设备上还没有登录苹果帐号。

需求:我们要在B上也登录QiShare帐号

步骤:当我们在B 设备上输入正确的QiShare帐号密码后,设备B上会提示,需要输入一个验证码,A 设备上会显示出B设备在什么位置要登录QiShare帐号,是否同意。而这个验证码会显示在A 设备上。

笔者对这里的双重因子的理解是:

1.1. 正确的AppleID 及相应密码; 1.2. 需要在已经登录过帐号密码的设备上同意登录请求并且提供验证码。

接着上边的描述,为了直观的表示相关信息,大家可以看下边的一组图,应该就能够理解笔者要表达的意思了。

  1. 在B设备上登录QiShare帐号,截图省略。

  2. 此时,已经登录过QiShare帐号的设备A的提示如下: twoFactorAPrompt

  3. 要登录QiShare帐号的设备B的提示: twoFactorBPrompt

  4. 已登录过QiShare帐号的设备A提示的验证码: twoFactorAVerificationCode

  5. 设备B的提示是否信任浏览器,如果信任了浏览器,以后就不再需要每次登录帐号,都需要A设备上同意。 twoFactorBelieveSafari

更多相关内容可查看:Two-factor authentication for Apple ID

2. 开发Sign In With Apple的注意事项:

Sign In With Apple 是iOS13 新增的功能,需要使用: MacOS 10.14.4或之后的Mac上的Xcode 11开发。

Xcode 11 includes SDKs for iOS 13, macOS Catalina 10.15, watchOS 6, and tvOS 13. Xcode 11 supports on-device debugging for iOS 8 and later, tvOS 9 and later, and watchOS 2 and later. Xcode 11 requires a Mac running macOS Mojave 10.14.4 or later.

Xcode 11 包含支持iOS13、macOS Catalina 10.15, watchOS 6, and tvOS 13的SDK,Xcode11 支持iOS8、tvOS2 或之后的设备,Xcode11 需要运行在MacOS 10.14.4或之后的Mac上。

Sign In With Apple 是跨平台的,可以支持iOS、macOS、watchOS、tvOS、JS。

####二、 iOS Sign In With Apple 流程

使用Sign In With Apple 的流程为:

  1. 设置ASAuthorizationAppleIDButton相关布局,添加相应地授权处理;
  2. 获取授权码;
  3. 验证;
  4. 处理Sign In With Apple授权状态变化;

下边笔者展开描述下iOS 使用Sign In With Apple的准备工作、可能遇到的问题及流程。

1. iOS 使用Sign In With Apple的准备工作:

1.1在Xcode11 Signing & Capabilities 中添加 Sign In With Apple

2. iOS 使用Sign In With Apple可能遇到的问题:

2.1 开启双重因子验证的方式:

  • 双重因子验证的开启:设置 -> 密码与安全性 -> 双重因子验证; 如果不开启双重因子验证,那么当我们在调用苹果官方授权接口的时候,系统也会提示我们需要去打开双重因子验证。

2.2 停止App 使用Sign In With Apple 的方式:

  • 停止App 使用Sign In With Apple:设置 -> 密码与安全性 -> 使用您AppleID的App -> 找到对应的App - > “停止以Apple ID使用 Bundle ID...”;

3. iOS使用Sign In With Apple 的开发流程:

在介绍布局之前先看下,笔者先给大家看下界面效果: Sign In With Apple 界面 界面比较简单,界面上方为UITextView用于展示授权状态授权信息,"Sign In With Apple Button" 是用的官方的ASAuthorizationAppleIDButton。中间的“移除键盘”按钮用于移除键盘。

3.1设置ASAuthorizationAppleIDButton相关布局,添加相应地授权处理;

- (void)setupUI {
    
    // 用于展示Sign In With Apple 登录过程的信息
    _appleIDInfoTextView = [[UITextView alloc] initWithFrame:CGRectMake(.0, 40.0, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame) * 0.4) textContainer:nil];
    _appleIDInfoTextView.font = [UIFont systemFontOfSize:32.0];
    [self.view addSubview:_appleIDInfoTextView];
    
    // 移除键盘Button
    UIButton *removeKeyboardBtn = [[UIButton alloc] init];
    removeKeyboardBtn.backgroundColor = [UIColor grayColor];
    [removeKeyboardBtn setTitle:@"移除键盘" forState:UIControlStateNormal];
    removeKeyboardBtn.frame = CGRectMake(CGRectGetMidX(_appleIDInfoTextView.frame) - 50.0, CGRectGetMaxY(_appleIDInfoTextView.frame), 100.0, 40.0);
    [removeKeyboardBtn addTarget:self action:@selector(removeFirstResponder:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:removeKeyboardBtn];

    if (@available(iOS 13.0, *)) {
    // Sign In With Apple Button
    ASAuthorizationAppleIDButton *appleIDButton = [ASAuthorizationAppleIDButton new];
        
    appleIDButton.frame =  CGRectMake(.0, .0, CGRectGetWidth(self.view.frame) - 40.0, 100.0);
    CGPoint origin = CGPointMake(20.0, CGRectGetMidY(self.view.frame));
    CGRect frame = appleIDButton.frame;
    frame.origin = origin;
    appleIDButton.frame = frame;
    appleIDButton.cornerRadius = CGRectGetHeight(appleIDButton.frame) * 0.25;
    [self.view addSubview:appleIDButton];
    [appleIDButton addTarget:self action:@selector(handleAuthrization:) forControlEvents:UIControlEventTouchUpInside];
    }
    
    NSMutableString *mStr = [NSMutableString string];
    [mStr appendString:@"显示Sign In With Apple 登录信息\n"];
    _appleIDInfoTextView.text = [mStr copy];
}

#pragma mark - Actions

//! 处理授权
- (void)handleAuthrization:(UIButton *)sender {
    if (@available(iOS 13.0, *)) {
        // A mechanism for generating requests to authenticate users based on their Apple ID.
        // 基于用户的Apple ID授权用户,生成用户授权请求的一种机制
        ASAuthorizationAppleIDProvider *appleIDProvider = [ASAuthorizationAppleIDProvider new];
        // Creates a new Apple ID authorization request.
        // 创建新的AppleID 授权请求
        ASAuthorizationAppleIDRequest *request = appleIDProvider.createRequest;
        // The contact information to be requested from the user during authentication.
        // 在用户授权期间请求的联系信息
        request.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
        // A controller that manages authorization requests created by a provider.
        // 由ASAuthorizationAppleIDProvider创建的授权请求 管理授权请求的控制器
        ASAuthorizationController *controller = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]];
        // A delegate that the authorization controller informs about the success or failure of an authorization attempt.
        // 设置授权控制器通知授权请求的成功与失败的代理
        controller.delegate = self;
        // A delegate that provides a display context in which the system can present an authorization interface to the user.
        // 设置提供 展示上下文的代理,在这个上下文中 系统可以展示授权界面给用户
        controller.presentationContextProvider = self;
        // starts the authorization flows named during controller initialization.
        // 在控制器初始化期间启动授权流
        [controller performRequests];
    }
}

关于ASAuthorizationAppleIDButton的设计规范,可以查看:Human Interface Guidelines 之 Sign In with Apple

3.2 获取授权码

获取授权码这部分主要看2个代理ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding,及相应的代理方法中的实现。

ASAuthorizationControllerDelegate An interface for providing information about the outcome of an authorization request. 提供关于授权请求结果信息的接口

ASAuthorizationControllerPresentationContextProviding: An interface the controller uses to ask a delegate for a presentation context. 控制器的代理找一个展示授权控制器的上下文的接口

下边为实现代理方法的代码:

#pragma mark - Delegate

//! 授权成功地回调
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization  API_AVAILABLE(ios(13.0)){
    
    NSLog(@"%s", __FUNCTION__);
    NSLog(@"%@", controller);
    NSLog(@"%@", authorization);
    
    NSLog(@"authorization.credential:%@", authorization.credential);
    
    NSMutableString *mStr = [NSMutableString string];
    mStr = [_appleIDInfoTextView.text mutableCopy];
    
    if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
        // 用户登录使用ASAuthorizationAppleIDCredential
        ASAuthorizationAppleIDCredential *appleIDCredential = authorization.credential;
        NSString *user = appleIDCredential.user;
        //  需要使用钥匙串的方式保存用户的唯一信息 这里暂且处于测试阶段 是否的NSUserDefaults
        [[NSUserDefaults standardUserDefaults] setValue:user forKey:QiShareCurrentIdentifier];
        [mStr appendString:user?:@""];
        NSString *familyName = appleIDCredential.fullName.familyName;
        [mStr appendString:familyName?:@""];
        NSString *givenName = appleIDCredential.fullName.givenName;
        [mStr appendString:givenName?:@""];
        NSString *email = appleIDCredential.email;
        [mStr appendString:email?:@""];
        NSLog(@"mStr:%@", mStr);
        [mStr appendString:@"\n"];
        _appleIDInfoTextView.text = mStr;
        
    } else if ([authorization.credential isKindOfClass:[ASPasswordCredential class]]) {
        // 用户登录使用现有的密码凭证
        ASPasswordCredential *passwordCredential = authorization.credential;
        // 密码凭证对象的用户标识 用户的唯一标识
        NSString *user = passwordCredential.user;
        // 密码凭证对象的密码
        NSString *password = passwordCredential.password;
        [mStr appendString:user?:@""];
        [mStr appendString:password?:@""];
        [mStr appendString:@"\n"];
        NSLog(@"mStr:%@", mStr);
        _appleIDInfoTextView.text = mStr;
    } else {
        NSLog(@"授权信息均不符");
        mStr = [@"授权信息均不符" mutableCopy];
        _appleIDInfoTextView.text = mStr;
    }
}

//! 授权失败的回调
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error  API_AVAILABLE(ios(13.0)){
    
    NSLog(@"%s", __FUNCTION__);
    NSLog(@"错误信息:%@", 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;
    }
    
    NSMutableString *mStr = [_appleIDInfoTextView.text mutableCopy];
    [mStr appendString:errorMsg];
    [mStr appendString:@"\n"];
    _appleIDInfoTextView.text = [mStr copy];
    
    if (errorMsg) {
        return;
    }
    
    if (error.localizedDescription) {
        NSMutableString *mStr = [_appleIDInfoTextView.text mutableCopy];
        [mStr appendString:error.localizedDescription];
        [mStr appendString:@"\n"];
        _appleIDInfoTextView.text = [mStr copy];
    }
    NSLog(@"controller requests:%@", controller.authorizationRequests);
    /* // 取消授权的时候也会调用这里
     ((ASAuthorizationAppleIDRequest *)(controller.authorizationRequests[0])).requestedScopes
     <__NSArrayI 0x2821e2520>(
     full_name,
     email
     )
     */
}


//! Tells the delegate from which window it should present content to the user.
//! 告诉代理应该在哪个window 展示内容给用户
- (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller  API_AVAILABLE(ios(13.0)){
    
    NSLog(@"调用展示window方法:%s", __FUNCTION__);
    // 返回window
    return self.view.window;
}

授权登录成功后调试的时候查看到的用户信息相关内容:

 po appleIDCredential.authorizationCode
<63636435 32316262 32666464 30346130 62616366 65336439 32636564 34383666 622e302e 6d727371 7a2e5853 47686543 5f354f6e 48786838 32766670 50484377>

(lldb) po appleIDCredential.user
0012xx.d81c1988bb054e91beec303a4xxxxxxx.0xx6

(lldb) po appleIDCredential.fullName
<NSPersonNameComponents: 0x281bf2070> {givenName = YW, familyName = W, middleName = (null), namePrefix = (null), nameSuffix = (null), nickname = (null) phoneticRepresentation = (null) }

(lldb) po appleIDCredential.email
26xxxxx168@qq.com

已经使用Sign In With Apple登录过app的用户

执行已经登录过的场景。如果设备中存在iCloud Keychain 凭证或者AppleID 凭证提示用户直接使用TouchID或FaceID登录即可。

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    
    [self perfomExistingAccountSetupFlows];
}

//! Prompts the user if an existing iCloud Keychain credential or Apple ID credential is found.
//! 如果存在iCloud Keychain 凭证或者AppleID 凭证提示用户
- (void)perfomExistingAccountSetupFlows {
    if (@available(iOS 13.0, *)) {
        // A mechanism for generating requests to authenticate users based on their Apple ID.
        // 基于用户的Apple ID授权用户,生成用户授权请求的一种机制
        ASAuthorizationAppleIDProvider *appleIDProvider = [ASAuthorizationAppleIDProvider new];
        // An OpenID authorization request that relies on the user’s Apple ID.
        // 授权请求依赖于用于的AppleID
        ASAuthorizationAppleIDRequest *authAppleIDRequest = [appleIDProvider createRequest];
        // A mechanism for generating requests to perform keychain credential sharing.
        // 为了执行钥匙串凭证分享生成请求的一种机制
        ASAuthorizationPasswordRequest *passwordRequest = [[ASAuthorizationPasswordProvider new] createRequest];
        
        NSMutableArray <ASAuthorizationRequest *>* mArr = [NSMutableArray arrayWithCapacity:2];
        if (authAppleIDRequest) {
            [mArr addObject:authAppleIDRequest];
        }
        if (passwordRequest) {
            [mArr addObject:passwordRequest];
        }
        // ASAuthorizationRequest:A base class for different kinds of authorization requests.
        // ASAuthorizationRequest:对于不同种类授权请求的基类
        NSArray <ASAuthorizationRequest *>* requests = [mArr copy];
        
        // A controller that manages authorization requests created by a provider.
        // 由ASAuthorizationAppleIDProvider创建的授权请求 管理授权请求的控制器
        // Creates a controller from a collection of authorization requests.
        // 从一系列授权请求中创建授权控制器
        ASAuthorizationController *authorizationController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:requests];
        // A delegate that the authorization controller informs about the success or failure of an authorization attempt.
        // 设置授权控制器通知授权请求的成功与失败的代理
        authorizationController.delegate = self;
        // A delegate that provides a display context in which the system can present an authorization interface to the user.
        // 设置提供 展示上下文的代理,在这个上下文中 系统可以展示授权界面给用户
        authorizationController.presentationContextProvider = self;
        // starts the authorization flows named during controller initialization.
        // 在控制器初始化期间启动授权流
        [authorizationController performRequests];
    }
}


3.3 Verification

关于验证的这一步,服务端同学LY和笔者都认为是需要传递授权码给自己的服务端,自己的服务端调用苹果APIGenerate and validate tokens去校验授权码。但是相关的API 调用不通。总是提示grant_type 有问题。不过我们服务端同学LY在PC端使用Sign In With Apple做过相应地测试,发现苹果提供的授权码校验的API是通的。当然相关内容也可能是笔者理解有误,大家如果有不同的理解,敬请讨论。

{
    "error": "unsupported_grant_type"
}

所以在这一步,笔者还没有较好的解决办法。大家需要使用相关功能的,可以根据业务场景,考虑下其他的处理方式。

3.4监听授权状态变化

监听授权状态改变,并且做出相应处理。授权状态有:

ASAuthorizationAppleIDProviderCredentialRevoked:授权状态失效(用户停止使用AppID 登录App)、 ASAuthorizationAppleIDProviderCredentialAuthorized:已授权(已使用AppleID 登录过App)、 ASAuthorizationAppleIDProviderCredentialNotFound:授权凭证缺失(可能是使用AppleID 登录过App)

处理改变有2种处理方式,一种是通过通知的方式,另一种是监听当前的appleIDCredential.user 的授权状态。

3.4.1 监听appleIDCredential.user 的授权状态,这部分代码可以放到AppDelegate的- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions中,判断是否需要展示出登录控制器。

#pragma mark - Private functions
//! 观察授权状态
- (void)observeAuthticationState {
    
    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 = [[NSUserDefaults standardUserDefaults] valueForKey:QiShareCurrentIdentifier];
        
        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:
                        // 苹果授权凭证失效
                        errorMsg = @"苹果授权凭证失效";
                        break;
                    case ASAuthorizationAppleIDProviderCredentialAuthorized:
                        // 苹果授权凭证状态良好
                        errorMsg = @"苹果授权凭证状态良好";
                        break;
                    case ASAuthorizationAppleIDProviderCredentialNotFound:
                        // 未发现苹果授权凭证
                        errorMsg = @"未发现苹果授权凭证";
                        // 可以引导用户重新登录
                        break;
                }
                dispatch_async(dispatch_get_main_queue(), ^{
                    NSLog(@"SignInWithApple授权状态变化情况");
                    NSLog(@"%@", errorMsg);
                });
            }];
            
        }
    }
}

3.4.2使用通知的方式检测是否授权应用支持Sign In With Apple变化情况。如下的代码可以根据自己的业务场景去考虑放置的位置。


//! 添加苹果登录的状态通知
- (void)observeAppleSignInState {
    if (@available(iOS 13.0, *)) {
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        [center addObserver:self selector:@selector(handleSignInWithAppleStateChanged:) name:ASAuthorizationAppleIDProviderCredentialRevokedNotification object:nil];
    }
}

//! 观察SignInWithApple状态改变
- (void)handleSignInWithAppleStateChanged:(id)noti {
    
    NSLog(@"%s", __FUNCTION__);
    NSLog(@"%@", noti);
}

- (void)dealloc {
    
    if (@available(iOS 13.0, *)) {
        [[NSNotificationCenter defaultCenter] removeObserver:self name:ASAuthorizationAppleIDProviderCredentialRevokedNotification object:nil];
    }
}

相关示意图如下:

  1. 首次使用AppleID登录或者停止使用AppleID登录后,再次使用Sign In With Apple的提示如下:

首次使用AppleID登录或者停止使用AppleID登录后再次使用Sign In With Apple

  1. 使用Sign In With Apple登录成功的截图如下: 使用Sign In With Apple登录成功

  2. 使用过AppleID登录过App,进入应用的时候会提示使用TouchID登录的场景如下: 使用过AppleID登录过App,进入应用的时候会提示使用TouchID登录的场景如下

  3. 使用Sign In With Apple登录成功的截图如下: 使用Sign In With Apple登录成功

Sign In with Apple will be available for beta testing this summer. It will be required as an option for users in apps that support third-party sign-in when it is commercially available later this year. Sign In With Apple 将在今年夏天可以用于beta版测试。对于支持三方登录的apps,在今年晚些时候iOS13的新设备出售的时候,Sign In With Apple 将被要求作为一种登录选择。 developer.apple.com/news/?id=06…

笔者对这段话的理解是,对于支持三方登录的应用,需提供Sign In With Apple选项登录。

Demo

QiSignInWithApple

Swift版官方Sign In With Apple

参考学习网址


推荐文章:
算法小专栏:动态规划(一)
Dart基础(一)
Dart基础(二)
Dart基础(三)
Dart基础(四)
iOS 短信验证码倒计时按钮
iOS 环境变量配置
iOS 中处理定时任务的常用方法
奇舞周刊