1.背景
根据苹果审核指南要求,但凡接入三方登录的,必须要接入苹果授权登录,因我们App中接入了微信登录,所以按照要求添加苹果登录。
苹果审核指南-4.8 通过 Apple 登录
如果 app 专门使用第三方或社交登录服务 (例如,Facebook 登录、Google 登录、通过 Twitter 登录、通过 LinkedIn 登录、通过 Amazon 登录或微信登录) 来对其进行设置或验证这个 app 的用户主帐户,则该 app 必须同时提供“通过 Apple 登录”作为等效选项。用户的主帐户是指在 app 中建立的、用于标识身份、登录和访问功能和相关服务的帐户。
在以下情况下,不要求提供“通过 Apple 登录”选项:
您的 app 仅使用公司自有的帐户设置和登录系统。
您的 app 是一款教育、企业或商务 app,要求用户使用现有的教育或企业帐户登录。
您的 app 使用政府或行业支持的公民身份系统或电子身份证来鉴定用户身份。
您的 app 是特定第三方服务的客户端,用户需要使用他们的邮件、社交媒体或其他第三方帐户直接登录才能访问内容。
2.接入准备工作
2.1 开发者网站,开启对应的Sign in With Apple
勾选Sign In With Apple,点击Edit
选中Enable as a primary App ID
2.2 创建用于后台生成client_secret的私钥
填写KeyName,勾选Sign In With Apple,点击Configure
选择对应的App ID,点击Save
选择continue
注册
点击Download,下载私钥,只能下载一次
你也可以返回Keys列表,点击刚才我们创建的Key,查看Key ID,复制保存,以备用
2.3. Xcode 开启Sign In With Apple
3.接入大致时序图
其实我们校验可以有两种方式:
方案一:是我们App自己校验,解析SDK返回的userid、identityToken,然后将userId给后台,后台进行数据库查询,返回绑定信息。这种比较简单。
方案二:是App拿到SDK返回的authorization_code,交给后台,后台通过https://appleid.apple.com/auth/token接口检验,返回id_token,解析token并返回绑定信息。这种相对复杂
4.iOS端工作
授权大致需要以下几步:
- 导入授权库#import <AuthenticationServices/AuthenticationServices.h>
- 添加响应事件,唤起Apple授权界面
- 验证密码或者生物ID
- 校验通过,将回调结果传递给后台
首先,添加事件按钮:
- (void)createAppleIDLoginButton{
if (@available(iOS 13.0, *)) {
self.appleLoginButton = [[ASAuthorizationAppleIDButton alloc] initWithAuthorizationButtonType:ASAuthorizationAppleIDButtonTypeSignIn authorizationButtonStyle:ASAuthorizationAppleIDButtonStyleWhiteOutline];
[self.appleLoginButton addTarget:self action:@selector(signinWithApple) forControlEvents:UIControlEventTouchUpInside];
[loginButtonView addSubview:self.appleLoginButton];
}
}
// 唤起苹果登录
- (void)signinWithApple API_AVAILABLE(ios(13.0)){
// 创建登录请求
ASAuthorizationAppleIDRequest *idRequest = [[[ASAuthorizationAppleIDProvider alloc] init] createRequest];
// 创建iCloud 密码填充登录,可不创建
ASAuthorizationPasswordRequest *passwordRequest = [[[ASAuthorizationPasswordProvider alloc] init] createRequest];
// 请求的用户数据
idRequest.requestedScopes = @[ASAuthorizationScopeFullName,ASAuthorizationScopeEmail];
// 如果不需要iCloud密码登录,不添加passwordRequest
ASAuthorizationController *controller = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[idRequest,passwordRequest]];
controller.delegate = self;
controller.presentationContextProvider = self;
[controller performRequests];
}
- (void)handleAppleResponse:(ASAuthorizationAppleIDCredential *)credential API_AVAILABLE(ios(13.0)){
// 将返回的数据,提交给后台
}
其次,遵循代理ASAuthorizationControllerDelegate
,ASAuthorizationControllerPresentationContextProviding
,并实现代理回调:
#pragma mark - ASAuthorizationControllerDelegate
- (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;
NSString *user = credential.user;
NSPersonNameComponents *fullName = credential.fullName;
NSString *email = credential.email;
NSString *authorizationCode = [[NSString alloc] initWithData:credential.authorizationCode encoding:NSUTF8StringEncoding]; // refresh token
NSString *identityToken = [[NSString alloc] initWithData:credential.identityToken encoding:NSUTF8StringEncoding]; // access token
ASUserDetectionStatus realUserStatus = credential.realUserStatus;
Log(@"state: %@", state);
Log(@"user: %@", user);
Log(@"fullName: %@", fullName);
Log(@"email: %@", email);
Log(@"authorizationCode: %@", authorizationCode);
Log(@"identityToken: %@", identityToken);
Log(@"realUserStatus: %@", @(realUserStatus));
// 数据处理
[self handleAppleResponse:credential];
}
//else if([authorization.credential isKindOfClass:ASPasswordCredential.class]) {
//ASPasswordCredential *credential = authorization.credential;
//Log(@"user:%@", credential.user);
//Log(@"password:%@", credential.password);
//}
}
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)){
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;
}
Log(@"%@", errorMsg);
}
#pragma mark - ASAuthorizationControllerPresentationContextProviding
- (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0)){
return self.window;
}
5.后台工作
接收iOS端授权结果并验证,向苹果服务请求id_token,大致流程为:
5.1. 提供一个接口接收user(唯一标识)、identityToken、authorizationCode,并进行签名验证;
使用苹果给出的Json Web Key,生成公钥,对客户端传过来的identityToken进行验签。
公钥地址:https://appleid.apple.com/auth/keys, 需要我们转化为公钥,公钥转换参考地址:https://8gwifi.org/jwkconvertfunctions.jsp
{
"keys": [
{
"kty": "RSA",
"kid": "AIDOPK1",
"use": "sig",
"alg": "RS256",
"n": "lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w",
"e": "AQAB"
}
]
}
要验证身份令牌,服务器必须:
使用服务器的公钥验证JWS E256签名
验证随机数以进行身份验证(authorizationcode)
验证iss字段包含https://appleid.apple.com
确认aud栏位是开发人员的client_id(bundle id)
验证时间早于令牌的exp值
5.2. 生成苹果服务器验证合法性所需JWT格式的client_secret;
client_secret生成所需参数:
privatekey:苹果开发者账号中创建,只能下载一次(.P8格式)需要妥善保管
alg:算法"ES256"
kid:私钥id,苹果开发者账号中创建的私钥对应的key_id
iss:team_id,苹果开发者账号中获取 Team ID
iat:密钥开始时间,UTC秒
exp:密钥到期时间,其值不得大于服务器上的当前Unix时间的15777000(6个月,以秒为单位)
aud:固定值"https://appleid.apple.com"
sub:bundle id
创建令牌后,使用带有P-256曲线的椭圆曲线数字签名算法(ECDSA)和SHA-256哈希算法对其进行签名。在算法标题键中指定值ES256。在kid属性中指定密钥标识符。
5.3. 对苹果后台通过https://appleid.apple.com/auth/token接口返回的数据和客户端通过接口传过来的数据进行校验
您最多可以每天验证一次刷新令牌,以确认该设备上用户的Apple ID在Apple的服务器上仍然保持良好的信誉。如果您尝试每天多次验证用户的Apple ID,Apple的服务器可能会限制您的通话。
You may verify the refresh token up to once a day to confirm that the user’s Apple ID on that device is still in good standing with Apple’s servers. Apple’s servers may throttle your call if you attempt to verify a user’s Apple ID more than once a day.
参数:
client_id:客户端bundle id字符串,授权、刷新token公用,必传。
client_secret:上一步生成的JWT签名字符串,授权、刷新token公用,必传。
code:客户端传过来的 authorizationcode,授权专用,一次性使用,有效期5分钟。
grant_type:客户端与服务器交互类型,固定字符串, 授权、刷新token公用,必传。授权传“authorization_code”,刷新Token用“refresh_token”。
refresh_token:刷新Token使用的令牌,刷新token时用。
redirect_uri:授权时用的重定向URL,web使用AppleID登录时使用。如果是使用web,开发者网站中配置sign in with appleid 时填写Web Authentication Configuration,原生可不传。
苹果后台返回的数据:
{
"access_token": "一个token,此处省略",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "一个token,此处省略",
"id_token": "结果是JWT,字符串形式,此处省略"
}
//示例:
{
"access_token": "ac6dd62539f5441cdacd7b548a9fe33a9.0.nrszq.gCD9GEmcznYjt5m3h4UkEg",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "r9f10xxxxxxxxxxxe80e.0.nxxxxq.fk7Q1ZxxxxxxxxxM0w",
"id_token": "eyJraWQiOiJlWGF1bm1MIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnpvZnVuZC5xaWFuZ3VuZ3VuIiwiZXhwIjoxNTgzMTMxNzI1LCJpYXQiOjE1ODMxMzExMjUsInN1YiI6IjAwMTI5MC44MDYzZGRmODMwYjI0YTQ5OTc4OTZhNmUxOGNmMjE5Yi4xMDEzIiwiYXRfaGFzaCI6IjBrU05fMzlkcGxhUEdnMUd0YV9Ka1EiLCJlbWFpbCI6InJlZXM5cGd3NWJAcHJpdmF0ZXJlbGF5LmFwcGxlaWQuY29tIiwiZW1haWxfdmVyaWZpZWQiOiJ0cnVlIiwiaXNfcHJpdmF0ZV9lbWFpbCI6InRydWUiLCJhdXRoX3RpbWUiOjE1ODMxMzEwOTV9.p19sc-tjsNQNCXyb33AX9r4oXj1xA4EmKh9Yp5E5ImxnOe6n_ISqvgMyDGqOuZwLAP9iMfB4-S_--1dpuzPx4HtwOyygpHhZSEZ4GcynCpHg6MFC7Mlkcn34J_awEXPeox_nJMRPRMN-ydQ7GxLvSrEJPJ-1rL473pIBc-DyNdYjkXcuyVU4FN6nEuh2NrOKCzMjkeEDqSmL2nG_TM7qE7JscAOcI6Nv5oml2KkYMeQl24kopQa2rC3m8HSsYSdaPs04pdiFEF20Fl3RqR-cnE0UeTmlC4KaBRF4xGpPpNT-OKvW2P6yUrkHmS27Mt1vM1sJkCiKMUGO3_i0Ef7ghA"
}
对id_token中的header、payload进行base64解码,获取相应的信息:
header:
{
"kid":"eXaunmL",
"alg":"RS256"
}
payload:
{
"iss":"https://appleid.apple.com",
"aud":"com.qiangungun",
"exp":1582880575,
"iat":1582879975,
"sub":"这个字段和客户端拿到的user以及identityToken第二段base64解码出来的sub相同",
"at_hash":"8_moKGpSUFG-zueTcf5EjQ",
"email":"vxxx@privaterelay.appleid.com",
"email_verified":"true",
"is_private_email":"true",
"auth_time":1582879944
}
通过对客户端传过来的user与payload中的sub进行对比,可以确定是否是统一用户
5.4. 校验通过,返回绑定状态或者注册状态
如果已经绑定过,返回sessionId、userId
如果未绑定过,返回类似于微信登录所需要信息,客户端进行绑定操作