在最新的审核指南中,出现了关于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服务触发.无论是那种方式,都需要一些基本的准备.
Certificates, Identifiers & Profiles
主屏幕,从侧面导航中选择Keys;
输入自定义字符串作为服务的标志,选择Sign in with Apple服务;
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
这部分主要用于通过加载链接的方式唤起appld账号登录授权,如果不需要实现这部分支持可以忽略.
Certificates, Identifiers & Profiles
主屏幕,从侧面导航中选择Identifiers;
主要是依靠原生拉起授权界面供用户登录交互,这部分实现官方给出了示例代码,可供参考.以下实现中使用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];
}
在这部分实现中允许应用通过加载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参数获取到授权码进行验证.
在原生的授权中,验证可以分为两步骤.
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保持一致,则第一步验证通过;否则验证失败.
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"
}
pem
对JWT进行验证identityToken
通过验证,则可以根据其payload中的内容进行验证等操作因为idnetityToken
使用非对称加密 RSASSA【RSA签名算法】 和 ECDSA【椭圆曲线数据签名算法】,当验证签名的时候,利用公钥来解密Singature,当解密内容与base64UrlEncode(header) + "." + base64UrlEncode(payload)
的内容完全一样的时候,表示验证通过。