Sign In With Apple

在最新的审核指南中,出现了关于Sign In With Apple 的要求:

  • 4.8 Sign in with Apple

    Apps that use a third-party or social login service (such as Facebook Login, Google Sign-In, Sign in with Twitter, Sign In with LinkedIn, Login with Amazon, or WeChat Login) to set up or authenticate the user’s primary account with the app must also offer Sign in with Apple as an equivalent option. A user’s primary account is the account they establish with your app for the purposes of identifying themselves, signing in, and accessing your features and associated services.

    Sign in with Apple is not required if:

    • Your app exclusively uses your company’s own account setup and sign-in systems.
    • Your app is an education, enterprise, or business app that requires the user to sign in with an existing education or enterprise account.
    • Your app uses a government or industry-backed citizen identification system or electronic ID to authenticate users.
    • Your app is a client for a specific third-party service and users are required to sign in to their mail, social media, or other third-party account directly to access their content.

除了规定的几种特殊情况之外,凡是使用第三方授权登录或者社交账号授权登录功能的应用都必须提供使用apple账号进行授权登录的功能.目前来讲提供了方式触发apple账号授权:原生触发和web服务触发.无论是那种方式,都需要一些基本的准备.

准备工作

在开发工具Xcode添加支持

  • 打开项目工程,在TARGETS-->选中需要添加Sign In With Apple功能的项目;
  • 选中Signing & Capabilities子栏;
  • 点击 "+Capabilitiy",搜索"Sign In With Apple"并添加.  新版本的Xcode会自动将该特性同步到开发者网站后台,如果不放心可以登录开发者后台进行手动同步.

           ​​

Sign In With Apple_第1张图片

获取移动端身份验证私钥

  • 登录开发者网站后台;
  • Certificates, Identifiers & Profiles主屏幕,从侧面导航中选择Keys;
  • 输入自定义字符串作为服务的标志,选择Sign in with Apple服务;

 

Sign In With Apple_第2张图片

  • 点击Configure按钮,选择服务绑定的应用对应的bundle identifier;

Sign In With Apple_第3张图片

  • 点击右上角"Save"按钮,继续点击右上角"Continue";

Sign In With Apple_第4张图片

  • 点击右上角"Register";

Sign In With Apple_第5张图片

  • 注册之后可获取到key ID,这个值会在生成访问密钥时会用到.你可以选择点击"Done"完成操作;也可以选择"Download"下载密钥.需要注意的是,这个密钥能下载且只能下载一次,如果丢失了就只能按照重新生成.下载之后会得到一个后缀为p8的密钥文件,请妥善保存.
  • 如果你想要进行移动校验测试,将如下脚本保存为secret_gen.rb,执行ruby secret_gen.rb获取到密钥client_secret.如果对移动端测试没有兴趣,直接将.p8文件发给后台就完事了.
require "jwt"
 
key_file = ""//刚才下载的.p8文件的路径
team_id = ""//TeamID,在开发者后台可以获取
client_id = "" //应用的bundleId
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

申请web端服务

这部分主要用于通过加载链接的方式唤起appld账号登录授权,如果不需要实现这部分支持可以忽略.

  • 登录开发者网站后台;
  • Certificates, Identifiers & Profiles主屏幕,从侧面导航中选择Identifiers;
  • 点击上方添加按钮,选择 "Services IDs"服务, 点击"Continue";

Sign In With Apple_第6张图片

  • 填写描述字符串(Description)和标记字符串(Identifier), 点击"Continue",在下一步中点击"Register".
  • 重新进入创建的Service ID服务,选择Sign In With Apple服务;

Sign In With Apple_第7张图片

  • 进行"Configure"进行配置,选择绑定服务的应用,并填写Domains and Subdomains以及Return URLs, 域名和回调链接可以填写多个在进行授权时选择其中一个中作为redirect_uri可以了,然后点击"Done";
  • 选择"Continue",并进行"Save".

原生触发登录授权

主要是依靠原生拉起授权界面供用户登录交互,这部分实现官方给出了示例代码,可供参考.以下实现中使用OC代码做说明,首先需要实现代理方法:

@interface LoginViewController ()
@property (weak, nonatomic) IBOutlet UIStackView *loginProviderStackView;

@end


//用户展示弹窗的父window
- (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller {
    return self.view.window;
}

/// - Tag: did_complete_authorization
-(void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)){
    if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
        ASAuthorizationAppleIDCredential * credential = authorization.credential;
        NSString *state = credential.state; //是ASAuthorizationRequest对象传递过来的值
        NSString * userIdentifier = credential.user; //apple系统的用户标识
        
        NSPersonNameComponents *fullName = credential.fullName;
        NSString * email = credential.email;
        //refresh token
        NSString * authorizationCode = [[NSString alloc]initWithData:credential.authorizationCode encoding:NSUTF8StringEncoding];
        // access token
        NSString * identityToken = [[NSString alloc]initWithData:credential.identityToken encoding:NSUTF8StringEncoding];
        ASUserDetectionStatus realUserStatus = credential.realUserStatus;
        NSLog(@"state: %@", state);
        NSLog(@"userID: %@", userIdentifier);
        NSLog(@"fullName: %@", fullName);
        NSLog(@"email: %@", email);
        NSLog(@"authorizationCode: %@", authorizationCode);
        NSLog(@"identityToken: %@", identityToken);
        NSLog(@"realUserStatus: %@", @(realUserStatus));
        
        
        /*
         https://appleid.apple.com/auth/token需要四个参数
         client_id(Required): 即应用的bundleId
         client_secret(Required): 使用.p8文件生成的密钥client_secret
         grant_type(Required): authorization_code
         code:authorizationCode
         */
        
    } else if([authorization.credential isKindOfClass:[ASPasswordCredential class]]) {
    //需要将credential保存在iCloud keychain中
        ASPasswordCredential *passwordCredential = (ASPasswordCredential *)authorization.credential;
        // Sign in using an existing iCloud Keychain credential.
        NSString *username = passwordCredential.user;
        NSString *password = passwordCredential.password;
        NSLog(@"username == %@, password == %@", username, password);

    }
}
/// - Tag: did_complete_error
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)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;
            
    }

}

在官方给出的实现代码中,分为两部分:

  • 第一次授权登录:
- (void)viewDidLoad {
    [super viewDidLoad];
    if (@available(iOS 13.0, *)) {
        [self setupProviderLoginView];
    }
    
    // Do any additional setup after loading the view.
}
- (void)setupProviderLoginView {
    ASAuthorizationAppleIDButton *authorizationButton = [ASAuthorizationAppleIDButton new];
    [authorizationButton addTarget:self action:@selector(handleAuthorizationAppleIDButtonPress) forControlEvents:UIControlEventTouchUpInside];
    [self.loginProviderStackView addArrangedSubview:authorizationButton];
}
- (void)handleAuthorizationAppleIDButtonPress {
    ASAuthorizationAppleIDProvider *appleIDProvider = [ASAuthorizationAppleIDProvider new];
    ASAuthorizationAppleIDRequest *request = [appleIDProvider createRequest];
    request.state = @"handleAuthorizationAppleIDButtonPress"; //会传递值到带来方法中
    request.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
    ASAuthorizationController *authorizationController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]];
    
    authorizationController.delegate = self;
    authorizationController.presentationContextProvider = self;
    [authorizationController performRequests];
    
}

如果出现了

[core] Authorization failed: Error Domain=AKAuthenticationError Code=-7026 "(null)" UserInfo={AKClientBundleID=com.xxxx.xxxx}

就说明项目中未添加对Sign In With Apple功能的支持,在TARGETS->项目-->Signing & Capabilities --> +Capability中进行添加即可.

  • 之前已经进行过登录授权
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    if (@available(iOS 13.0, *)) {
//这个方法只有在登录授权成功一次之后,且授权没有被取消没有被移除(Revoke)时才会起到作用弹出面容或者指纹识别登录,否则该方法没有任何作用
        [self performExistingAccountSetupFlows];
    }
}
- (void)performExistingAccountSetupFlows {
    // Prepare requests for both Apple ID and password providers.
    NSArray *requests = @[[[ASAuthorizationAppleIDProvider new] createRequest],
                                                    [[ASAuthorizationPasswordProvider new] createRequest]];
    
    // Create an authorization controller with the given requests.
    ASAuthorizationController *authorizationController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:requests];
    authorizationController.delegate = self;
    authorizationController.presentationContextProvider = self;
    [authorizationController performRequests];
    
}

web触发登录授权

在这部分实现中允许应用通过加载URL的方式获取apple账号登录授权,需要在开发者后台创建Service ID, 然后在Return URLs中填入可用的链接URL作为回调地址redirect_uri.例如使用https://www.baidu.com,可以在浏览器中加载如下链接触发apple账号登录授权:

https://appleid.apple.com/auth/authorize?response_type=code&redirect_uri=https%3a%2f%2fwww.baidu.com&client_id=`service ID 对应的bundleID`

需要注意的是redirect_uri参数对应的回调地址需要进行encode编码,然后就可以正常触发apple账号登录服务,在完整授权之后,web界面会加载回调链接并将授权code作为参数拼接在回调地址之后供验证使用:

https://www.baidu.com/?code=xxxx.0.nrtzw.yyyy

这样web页面就可以通过获取query中的code参数获取到授权码进行验证.

授权验证

在原生的授权中,验证可以分为两步骤.

  • 移动端验证:如果授权成功,在ASAuthorizationControllerDelegate代理方法authorizationController:didCompleteWithAuthorization:中可以获取到authorizationCode和identityToken两个参数
NSData *dataWithBase64String(NSString *payload) {
    NSData *data = nil;
    unsigned char *decodedBytes = NULL;
    @try {
#define __ 255
        static char decodingTable[256] = {
            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0x00 - 0x0F
            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0x10 - 0x1F
            __,__,__,__, __,__,__,__, __,__,__,62, __,__,__,63,  // 0x20 - 0x2F
            52,53,54,55, 56,57,58,59, 60,61,__,__, __, 0,__,__,  // 0x30 - 0x3F
            __, 0, 1, 2,  3, 4, 5, 6,  7, 8, 9,10, 11,12,13,14,  // 0x40 - 0x4F
            15,16,17,18, 19,20,21,22, 23,24,25,__, __,__,__,__,  // 0x50 - 0x5F
            __,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,  // 0x60 - 0x6F
            41,42,43,44, 45,46,47,48, 49,50,51,__, __,__,__,__,  // 0x70 - 0x7F
            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0x80 - 0x8F
            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0x90 - 0x9F
            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0xA0 - 0xAF
            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0xB0 - 0xBF
            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0xC0 - 0xCF
            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0xD0 - 0xDF
            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0xE0 - 0xEF
            __,__,__,__, __,__,__,__, __,__,__,__, __,__,__,__,  // 0xF0 - 0xFF
        };
        encoding = [encoding stringByReplacingOccurrencesOfString:@"=" withString:@""];
        NSData *encodedData = [encoding dataUsingEncoding:NSASCIIStringEncoding];
        unsigned char *encodedBytes = (unsigned char *)[encodedData bytes];
        
        NSUInteger encodedLength = [encodedData length];
        if( encodedLength >= (NSUIntegerMax - 3) ) return nil; // NSUInteger overflow check
        NSUInteger encodedBlocks = (encodedLength+3) >> 2;
        NSUInteger expectedDataLength = encodedBlocks * 3;
        
        unsigned char decodingBlock[4];
        
        decodedBytes = malloc(expectedDataLength);
        if( decodedBytes != NULL ) {
            
            NSUInteger i = 0;
            NSUInteger j = 0;
            NSUInteger k = 0;
            unsigned char c;
            while( i < encodedLength ) {
                c = decodingTable[encodedBytes[i]];
                i++;
                if( c != __ ) {
                    decodingBlock[j] = c;
                    j++;
                    if( j == 4 ) {
                        decodedBytes[k] = (decodingBlock[0] << 2) | (decodingBlock[1] >> 4);                
                        decodedBytes[k+1] = (decodingBlock[1] << 4) | (decodingBlock[2] >> 2);
                        decodedBytes[k+2] = (decodingBlock[2] << 6) | (decodingBlock[3]);
                        j = 0;
                        k += 3;
                    }
                }
            }
            
            // Process left over bytes, if any
            if( j == 3 ) {
                decodedBytes[k] = (decodingBlock[0] << 2) | (decodingBlock[1] >> 4);                
                decodedBytes[k+1] = (decodingBlock[1] << 4) | (decodingBlock[2] >> 2);
                k += 2;
            } else if( j == 2 ) {
                decodedBytes[k] = (decodingBlock[0] << 2) | (decodingBlock[1] >> 4);                
                k += 1;
            }
            data = [[NSData alloc] initWithBytes:decodedBytes length:k];
        }
    }
    @catch (NSException *exception) {
        data = nil;
        NSLog(@"WARNING: error occured while decoding base 32 string: %@", exception);
    }
    @finally {
        if( decodedBytes != NULL ) {
            free( decodedBytes );
        }
    }
    return data;
}


NSString * authorizationCode = [[NSString alloc]initWithData:credential.authorizationCode encoding:NSUTF8StringEncoding]; 
NSString * identityToken = [[NSString alloc]initWithData:credential.identityToken encoding:NSUTF8StringEncoding];
NSArray  *JWTResult = [identityToken componentsSeparatedByString:@"."];  
if (JWTResult.count == 3) {
    NSString *payload = [JWTResult objectAtIndex:1];
    NSError *error = nil;
    NSDictionary *content =  [NSJSONSerialization JSONObjectWithData: dataWithBase64String(payload) options:(0) error:&error];
    NSString *sub = content[@"sub"];
    NSLog(@"content == %@, sub == %@", content, sub);
    NSAssert([sub isEqualToString:userID], @"userIdentifier[sign in with apple]验证不通过");
    }

根据JWT的验证规则,取出payload中的sub参数部分,如果和userIdentifier保持一致,则第一步验证通过;否则验证失败.

  • apple后台验证:验证有两种方式,可以使用验证authorization_code或者验证identityToken
    • 验证authorization_code,apple提供了后台验证的接口供签权验证
POST https://appleid.apple.com/auth/token
Content-Type: application/x-www-form-urlencoded


{ 
    client_id: 即signInWithApple服务所在应用对应的bundleId
    client_secret: 生成的密钥 //使用ruby secret_gen.rb生成的密钥
    grant_type: authorization_code 
    code:`authorizationCode`
}

然后既可以获取到结果,大概长这个样子:

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

校验sub字段是否与原始获取到的useridentifier一致即可.需要注意的是authorizationCode只能使用一次而且有时间限制,如果已经使用进行过authorizationCode进行验证或者超出有效期,验证会失败:

{
    "error": "invalid_grant"
}

 

  • 验证identityToken
    • 需要逆向构造过程,decode出JWT的三个部分
    • 从https://appleid.apple.com/auth/keys中获取公钥,并将公钥转换为pem对JWT进行验证
    • identityToken通过验证,则可以根据其payload中的内容进行验证等操作

因为idnetityToken使用非对称加密 RSASSA【RSA签名算法】 和 ECDSA【椭圆曲线数据签名算法】,当验证签名的时候,利用公钥来解密Singature,当解密内容与base64UrlEncode(header) + "." + base64UrlEncode(payload) 的内容完全一样的时候,表示验证通过。

你可能感兴趣的:(iOS,开发)