适用于 Apple 平台的 GTMAppAuth
通过提供用于使用 AppAuth 认证请求的实现,GTMAppAuth 允许您在 iOS、macOS、tvOS 和 watchOS 上使用 AppAuth 与 Google Toolbox for Mac - Session Fetcher 以及 Google APIs Client Library for Objective-C For REST 库。
GTMAppAuth 是 GTMOAuth2 的替代认证器。关键区别在于使用用户的默认浏览器进行授权,这更加安全,更易用(用户的会话可以被重复使用),并遵循原生应用的现代 OAuth 最佳实践。还提供 GTMOAuth2 的兼容性方法,允许您在保留先前序列化的认证的情况下从 GTMOAuth2 迁移到 GTMAppAuth(因此用户不需要重新进行认证)。
设置
如果您使用 CocoaPods,只需添加
pod 'GTMAppAuth'
将 Podfile
替换并运行 pod install
。
用法
配置
要使用 Google 的 OAuth 端点配置 GTMAppAuth,您可以使用简便方法
OIDServiceConfiguration *configuration = [GTMAuthSession configurationForGoogle];
或者直接指定端点配置 GTMAppAuth
NSURL *authorizationEndpoint =
[NSURL URLWithString:@"https://accounts.google.com/o/oauth2/v2/auth"];
NSURL *tokenEndpoint =
[NSURL URLWithString:@"https://www.googleapis.com/oauth2/v4/token"];
OIDServiceConfiguration *configuration =
[[OIDServiceConfiguration alloc]
initWithAuthorizationEndpoint:authorizationEndpoint
tokenEndpoint:tokenEndpoint];
// perform the auth request...
或通过发现
NSURL *issuer = [NSURL URLWithString:@"https://accounts.google.com"];
[OIDAuthorizationService discoverServiceConfigurationForIssuer:issuer
completion:^(OIDServiceConfiguration *_Nullable configuration,
NSError *_Nullable error) {
if (!configuration) {
NSLog(@"Error retrieving discovery document: %@",
[error localizedDescription]);
return;
}
// perform the auth request...
}];
授权
首先,您需要一种方法,让您的 UIApplicationDelegate 能够从传入的重定向 URI 中继续授权流程会话。通常,您可以将正在进行的 OIDAuthorizationFlowSession 实例存储在一个属性中
// property of the app's UIApplicationDelegate
@property(nonatomic, nullable)
id<OIDExternalUserAgentSession> currentAuthorizationFlow;
并且在一个所有需要授权的控制器的可访问位置,一个属性存储授权状态
// property of the containing class
@property(nonatomic, nullable) GTMAuthSession *authSession;
然后,发起授权请求。通过使用 authStateByPresentingAuthorizationRequest
方法,OAuth 令牌交换将自动执行,并且所有内容都将受到 PKCE (如果服务器支持它) 的保护。
// builds authentication request
OIDAuthorizationRequest *request =
[[OIDAuthorizationRequest alloc] initWithConfiguration:configuration
clientId:kClientID
clientSecret:kClientSecret
scopes:@[OIDScopeOpenID, OIDScopeProfile]
redirectURL:redirectURI
responseType:OIDResponseTypeCode
additionalParameters:nil];
// performs authentication request
self.appDelegate.currentAuthorizationFlow =
[OIDAuthState authStateByPresentingAuthorizationRequest:request
callback:^(OIDAuthState *_Nullable authState,
NSError *_Nullable error) {
if (authState) {
// Creates a GTMAuthSession from the OIDAuthState.
self.authSession = [[GTMAuthSession alloc] initWithAuthState:authState];
NSLog(@"Got authorization tokens. Access token: %@",
authState.lastTokenResponse.accessToken);
} else {
NSLog(@"Authorization error: %@", [error localizedDescription]);
self.authSession = nil;
}
}];
处理重定向
授权响应 URL 通过平台特定的应用程序代理方法返回给应用程序,因此您需要将其传递到当前授权会话(创建在之前的会话中)。
macOS自定义URI方案重定向示例
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Other app initialization code ...
// Register for GetURL events.
NSAppleEventManager *appleEventManager =
[NSAppleEventManager sharedAppleEventManager];
[appleEventManager setEventHandler:self
andSelector:@selector(handleGetURLEvent:withReplyEvent:)
forEventClass:kInternetEventClass
andEventID:kAEGetURL];
}
- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
NSString *URLString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
NSURL *URL = [NSURL URLWithString:URLString];
[_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:URL];
}
iOS自定义URI方案重定向示例
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<NSString *, id> *)options {
// Sends the URL to the current authorization flow (if any) which will
// process it if it relates to an authorization response.
if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) {
_currentAuthorizationFlow = nil;
return YES;
}
// Your additional URL handling (if any) goes here.
return NO;
}
调用API
GTMAppAuth的目标是让您按照Session Fetcher模式使用新鲜令牌授权HTTP请求,您可以像这样操作
// Creates a GTMSessionFetcherService with the authorization.
// Normally you would save this service object and re-use it for all REST API calls.
GTMSessionFetcherService *fetcherService = [[GTMSessionFetcherService alloc] init];
fetcherService.authorizer = self.authSession;
// Creates a fetcher for the API call.
NSURL *userinfoEndpoint = [NSURL URLWithString:@"https://www.googleapis.com/oauth2/v3/userinfo"];
GTMSessionFetcher *fetcher = [fetcherService fetcherWithURL:userinfoEndpoint];
[fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
// Checks for an error.
if (error) {
// OIDOAuthTokenErrorDomain indicates an issue with the authorization.
if ([error.domain isEqual:OIDOAuthTokenErrorDomain]) {
self.authSession = nil;
NSLog(@"Authorization error during token refresh, clearing state. %@",
error);
// Other errors are assumed transient.
} else {
NSLog(@"Transient error during token refresh. %@", error);
}
return;
}
// Parses the JSON response.
NSError *jsonError = nil;
id jsonDictionaryOrArray =
[NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
// JSON error.
if (jsonError) {
NSLog(@"JSON decoding error %@", jsonError);
return;
}
// Success response!
NSLog(@"Success: %@", jsonDictionaryOrArray);
}];
保存到Keychain
您可以使用GTMKeychainStore
类轻松将GTMAuthSession
实例保存到Keychain。
// Create a GIDKeychainStore instance, intializing it with the Keychain item name `kKeychainItemName`
// which will be used when saving, retrieving, and removing `GTMAuthSession` instances.
GIDKeychainStore *keychainStore = [[GIDKeychainStore alloc] initWithItemName:kKeychainItemName];
NSError *error;
// Save to the Keychain
[keychainStore saveAuthSession:self.authSession error:&error];
if (error) {
// Handle error
}
// Retrieve from the Keychain
self.authSession = [keychainStore retrieveAuthSessionWithError:&error];
if (error) {
// Handle error
}
// Remove from the Keychain
[keychainStore removeAuthSessionWithError:&error];
if (error) {
// Handle error
}
Keychain存储
使用 GTMKeychainStore
时,默认情况下,GTMAuthSession
实例使用 kSecClassGenericPassword
类的钥匙串项目来存储,其 kSecAttrAccount
值为 "OAuth" 和开发者提供的 kSecAttrService
值。对于这种通用密码项目的使用,账户和服务值的组合充当钥匙串项目的 主键。设置 kSecAttrAccessible
键为 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
以允许重启后设备首次解锁后的后台访问。相关 GTMAuthSession
实例的键值归档表示作为 kSecValueData
的值提供给,并由 键链服务 加密并存储。
对于 macOS,有两个钥匙串存储选项:传统的基于文件的钥匙串存储,它使用访问控制列表,以及更现代的 数据保护钥匙串存储,它使用钥匙串访问控制组。默认情况下,GTMAppAuth 在 macOS 上使用基于文件的钥匙串存储。您可以通过在初始化 GTMKeychainStore
时在 initWithItemName:keychainAttributes:
参数中包含 GTMKeychainAttribute.useDataProtectionKeychain
属性来选择使用数据保护钥匙串存储。请注意,通过一种存储类型存储的钥匙串项目将无法通过另一种类型访问,选择使用数据保护钥匙串的 macOS 应用需要签名才能使钥匙串操作成功。
实现您自己的存储
如果您想使用除钥匙串之外的后备存储来保存您的 GTMAuthSession
实例,您可以创建自己的 GTMAuthSessionStore
合规。以 GTMKeychainStore
作为如何做到这一点的例子。
GTMOAuth2 兼容性
为了帮助从 GTMOAuth2 迁移到 GTMAppAuth,GTMKeychainStore
中提供了与 GTMOAuth2 兼容的钥匙串方法。
GTMKeychainStore keychainStore = [[GTMKeychainStore alloc] initWithItemName:kKeychainItemName];
// Retrieve from the Keychain
NSError *error;
GTMAuthSession *authSession =
[keychainStore retrieveAuthSessionForGoogleInGTMOAuth2FormatWithClientID:clientID
clientSecret:clientSecret
error:&error];
// Remove from the Keychain
[keychainStore removeAuthSessionWithError:&error];
您还可以保存到 GTMOAuth2 格式,尽管不鼓励这样做(您应使用上述描述的 GTMAppAuth 格式进行保存)。
// Save to the Keychain
[keychainStore saveWithGTMOAuth2FormatForAuthSession:authSession error:&error];
包含的样本
在示例下尝试其中的一个包含样本应用。在应用文件夹中运行pod install
,然后打开生成的xcworkspace
文件。
请确保按照Example-iOS/README.md或Example-macOS/README.md中的说明来配置用于示例的自定义OAuth客户端ID。
与GTMOAuth2的区别
授权方法
GTMAppAuth使用浏览器来展示授权请求,而GTMOAuth2则使用嵌入的web-view。迁移到GTMAppAuth将需要改变用户授权的方式。按照上面的说明获取授权。然后您可以创建一个带有initWithAuthState:
初始化器的GTMAuthSession
对象。一旦有了GTMAuthSession
对象,您可以像以前一样继续进行REST调用。
错误处理
GTMAppAuth的错误处理也不同。没有通知,您需要检查回调中的NSError。如果错误域名是OIDOAuthTokenErrorDomain
,表示授权错误,您应该清除授权状态并考虑提示用户重新授权。其他错误通常被视为暂时性的,这意味着您应该在延迟后重试请求。
序列化
GTMOAuth2 和 GTMAppAuth 之间的序列化格式不同,尽管我们有方法帮助您在不丢失任何数据的情况下从一个迁移到另一个。
从 GTMOAuth2 迁移
Oauth 客户端注册
通常,GTMOAuth2 客户端以“其他”类型在 Google 上注册。相反,Apple 客户端应注册为“iOS”类型。
如果您正在将一个 Apple 客户端迁移到与您的现有客户端相同的 项目中,请 注册一个新的 iOS 客户端 以用于 GTMAppAuth。
改变您的授权流程
GTMOAuth2 和 GTMAppAuth 都支持 GTMFetcherAuthorizationProtocol
,这允许您使用会话检索器进行授权。在您之前有一个如 GTMOAuth2Authentication *authorization
的属性时,将类型更改为引用该协议,即: id<GTMFetcherAuthorizationProtocol> authorization
。这允许您在底层切换授权实现到 GTMAppAuth。
接下来,按照上面的说明,将授权请求(即您要求用户授予访问权限的操作)替换为GTMAppAuth方法。如果您创建了一个新的OAuth客户端,请使用该客户端进行这些请求。
序列化 & 将现有授权迁移
GTMAppAuth有一个新的序列化数据和API格式。与GTMOAuth2不同,GTMAppAuth会将授权的配置和历史记录序列化,包括客户端ID和授权请求记录,该请求导致了授权授予。
用于GTMAppAuth的客户端ID与用于GTMOAuth2的不同。为了跟踪新旧授权中使用的不同客户端ID,建议迁移到新的序列化格式,它将为您保存这些信息。也提供GTMOAuth2兼容的序列化,但并非完全支持。
通过使用GTMAuthSession
和GTMKeychainStore
以下方式,更改您对authorization
对象的序列化方式
// Create an auth session from AppAuth's auth state object
GTMAuthSession *authSession = [[GTMAuthSession alloc] initWithAuthState:authState];
// Create a keychain store
GTMKeychainStore keychainStore = [[GTMKeychainStore alloc] initWithItemName:kNewKeychainName];
// Serialize to Keychain
NSError *error;
[keychainStore saveAuthSession:authSession error:&error];
务必为密钥链使用一个新的名称。不要重复使用旧的名称!
对于反序列化,我们可以保留所有现有的授权(因此,在GTMOAuth2中授权了您应用的用户无需再次授权)。记住,在反序列化古老的数据时,您需要使用您古老的密钥链名称,以及老的客户端ID和客户端密钥(如果那些改变了),并且,在序列化到新的格式时,使用新的密钥链名称。再次提醒,特别小心在反序列化GTMOAuth2密钥链时使用旧详情,以及在其他所有GTMAppAuth调用中使用新详情。
密钥链迁移示例
// Create a keychain store
GTMKeychainStore keychainStore = [[GTMKeychainStore alloc] initWithItemName:kNewKeychainName];
// Attempt to deserialize from Keychain in GTMAppAuth format.
NSError *error;
GTMAuthSesion *authSession =
[keychainStore retrieveAuthSessionWithError:&error];
// If no data found in the new format, try to deserialize data from GTMOAuth2
if (!authSession) {
// Tries to load the data serialized by GTMOAuth2 using old keychain name.
// If you created a new client id, be sure to use the *previous* client id and secret here.
GTMKeychainStore oldKeychainStore = [[GTMKeychainStore alloc] initWithItemName:kPreviousKeychainName];
authSession =
[oldKeychainStore retrieveAuthSessionInGTMOAuth2FormatWithClientID:kPreviousClientID
clientSecret:kPreviousClientSecret
error:&error];
if (authSession) {
// Remove previously stored GTMOAuth2-formatted data.
[oldKeychainStore removeAuthSessionWithError:&error];
// Serialize to Keychain in GTMAppAuth format.
[keychainStore saveAuthSession:authSession error:&error];
}
}