GTMAppAuth 允许您将 AppAuth 与 Google Toolbox for Mac - Session Fetcher 和 Google APIs Client Library for Objective-C For REST 库一起使用,这些库适用于 iOS、macOS、tvOS 和 watchOS。 通过为 AppAuth 授权请求提供 GTMFetcherAuthorizationProtocol
的实现来实现此目的。
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://#/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://#"];
[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 通过平台特定的应用程序委托方法返回到应用程序,因此您需要将其通过管道传输到当前的授权会话(在前一个会话中创建)。
- (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];
}
- (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;
}
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);
}];
您可以使用 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
}
使用 GTMKeychainStore
,默认情况下,GTMAuthSession
实例使用 kSecClassGenericPassword
类的 Keychain 项目存储,kSecAttrAccount
值为“OAuth”,并且开发者为 kSecAttrService
提供一个值。 对于通用密码项的这种使用,帐户和服务值的组合充当 Keychain 项目的主键。kSecAttrAccessible
键设置为 kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
,以便在重新启动后设备首次解锁后允许后台访问。 相关 GTMAuthSession
实例的 键归档表示形式作为 kSecValueData
的值提供,并由 Keychain Services 加密和存储。
对于 macOS,有两种 Keychain 存储选项可用:传统的基于文件的 Keychain 存储(使用访问控制列表)以及更现代的 数据保护 Keychain 存储(使用 Keychain 访问控制组)。 默认情况下,GTMAppAuth 在 macOS 上使用基于文件的 Keychain 存储。 您可以选择通过在初始化 GTMKeychainStore
时,在 initWithItemName:keychainAttributes:
的 keychainAttributes
参数中包含 GTMKeychainAttribute.useDataProtectionKeychain
属性来选择使用数据保护 Keychain 存储。 请注意,通过一种存储类型存储的 Keychain 项目将无法通过另一种存储类型访问,并且选择使用数据保护 Keychain 的 macOS 应用程序需要签名才能使 Keychain 操作成功。
如果您想使用 Keychain 以外的后备存储来保存您的 GTMAuthSession
实例,您可以创建自己的 GTMAuthSessionStore
一致性。 使用 GTMKeychainStore
作为如何执行此操作的示例。
为了帮助从 GTMOAuth2 迁移到 GTMAppAuth,GTMKeychainStore
中提供了 GTMOAuth2 兼容的 Keychain 方法。
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];
在 Examples 下尝试其中一个包含的示例应用程序。 在 apps 文件夹中运行 pod install
,然后打开生成的 xcworkspace
文件。
请务必按照 Example-iOS/README.md 或 Example-macOS/README.md 中的说明配置您自己的 OAuth 客户端 ID 以用于该示例。
GTMAppAuth 使用浏览器来呈现授权请求,而 GTMOAuth2 使用嵌入式 Web 视图。 迁移到 GTMAppAuth 将需要您更改授权用户的方式。 按照上述说明获取授权。 然后,您可以使用其 initWithAuthState:
初始化程序创建一个 GTMAuthSession
对象。 获得 GTMAuthSession
后,您可以继续像以前一样发出 REST 调用。
GTMAppAuth 的错误处理也不同。 没有通知,您需要检查回调中的 NSError。 如果错误域是 OIDOAuthTokenErrorDomain
,则表示授权错误,您应该清除您的授权状态并考虑提示用户再次授权。 其他错误通常被认为是暂时的,这意味着您应该在延迟后重试请求。
GTMOAuth2 和 GTMAppAuth 之间的序列化格式不同,但我们有方法可以帮助您从一种格式迁移到另一种格式,而不会丢失任何数据。
通常,GTMOAuth2 客户端在 Google 中注册为“Other”类型。 相反,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 不同。 为了跟踪新授权和旧授权使用的不同客户端 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];
请务必为 Keychain 使用*新*名称。 不要重用你的旧名字!
对于反序列化,我们可以保留所有现有授权(因此在 GTMOAuth2 中授权您的应用程序的用户不必再次授权)。 请记住,在反序列化*旧*数据时,您需要使用*旧*Keychain 名称,以及旧的客户端 ID 和客户端密钥(如果这些已更改),并且在序列化为*新*格式时,使用*新*Keychain 名称。 再次强调,请特别注意在反序列化 GTMOAuth2 Keychain 时使用旧的详细信息,以及在所有其他 GTMAppAuth 调用中使用新的详细信息。
Keychain 迁移示例
// 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];
}
}