Version Platform License tests

适用于 Apple 平台的 GTMAppAuth

GTMAppAuth 允许您将 AppAuthGoogle Toolbox for Mac - Session FetcherGoogle 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 通过平台特定的应用程序委托方法返回到应用程序,因此您需要将其通过管道传输到当前的授权会话(在前一个会话中创建)。

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 类的 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 兼容性

为了帮助从 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.mdExample-macOS/README.md 中的说明配置您自己的 OAuth 客户端 ID 以用于该示例。

与 GTMOAuth2 的区别

授权方法

GTMAppAuth 使用浏览器来呈现授权请求,而 GTMOAuth2 使用嵌入式 Web 视图。 迁移到 GTMAppAuth 将需要您更改授权用户的方式。 按照上述说明获取授权。 然后,您可以使用其 initWithAuthState: 初始化程序创建一个 GTMAuthSession 对象。 获得 GTMAuthSession 后,您可以继续像以前一样发出 REST 调用。

错误处理

GTMAppAuth 的错误处理也不同。 没有通知,您需要检查回调中的 NSError。 如果错误域是 OIDOAuthTokenErrorDomain,则表示授权错误,您应该清除您的授权状态并考虑提示用户再次授权。 其他错误通常被认为是暂时的,这意味着您应该在延迟后重试请求。

序列化

GTMOAuth2 和 GTMAppAuth 之间的序列化格式不同,但我们有方法可以帮助您从一种格式迁移到另一种格式,而不会丢失任何数据。

从 GTMOAuth2 迁移

OAuth 客户端注册

通常,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 兼容的序列化,但未完全支持。

通过使用 GTMAuthSessionGTMKeychainStore 如下所示来更改序列化 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];
  }
}