前言
筆者最近了解了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. 需要在已經登錄過帳號密碼的設備上同意登錄請求並且提供驗證碼。
接着上邊的描述,為了直觀的表示相關信息,大家可以看下下邊的一組圖,應該就能夠理解筆者要表達的意思了。
- 在B設備上登錄QiShare帳號,截圖省略。
- 此時,已經登錄過QiShare帳號的設備A的提示如下:

- 要登錄QiShare帳號的設備B的提示:

- 已登錄過QiShare帳號的設備A提示的驗證碼:

- 設備B的提示是否信任瀏覽器,如果信任了瀏覽器,以后就不再需要每次登錄帳號,都需要A設備上同意。

更多相關內容可查看:Two-factor authentication for Apple ID
2. 開發SignInWithApple的注意事項:
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 的流程為:
- 設置ASAuthorizationAppleIDButton相關布局,添加相應地授權處理;
- 獲取授權碼;
- 驗證;
- 處理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 的開發流程:
在介紹布局之前先看下,筆者先給大家看下界面效果:

界面比較簡單,界面上方為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];
}
}
相關示意圖如下:
- 首次使用AppleID登錄或者停止使用AppleID登錄后再次使用Sign In With Apple的提示如下:

- 使用Sign In With Apple登錄成功的截圖如下:

- 使用過AppleID登錄過App,進入應用的時候會提示使用TouchID登錄的場景如下:

- 使用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 將被要求作為一種登錄選擇。
https://developer.apple.com/news/?id=06032019j
筆者對這段話的理解是,對於支持三方登錄的應用,需提供Sign In With Apple選項登錄。
