向 iOS 应用添加“使用 Google 账号登录”功能

1. 准备工作

此 Codelab 将指导您构建一个实现“使用 Google 账号登录”功能并在模拟器中运行的 iOS 应用。我们提供了使用 SwiftUI 和 UIKit 的实现。

SwiftUI 是 Apple 针对新应用开发推出的现代界面框架。它支持使用单个共享代码库为所有 Apple 平台构建界面。它需要最低 iOS 版本 13。

UIKit 是 Apple 针对 iOS 推出的原始基础界面框架。它可针对旧版 iOS 提供向后兼容性。因此,对于需要支持各种旧版设备的成熟应用,这是一种不错的选择。

您可以选择最符合自己开发需求的框架路径。

前提条件

学习内容

  • 如何创建 Google Cloud 项目
  • 如何在 Google Cloud 控制台中创建 OAuth 客户端
  • 如何为 iOS 应用实现“使用 Google 账号登录”功能
  • 如何自定义“使用 Google 账号登录”按钮
  • 如何解码 ID 令牌
  • 如何为 iOS 应用启用 App Check

所需条件

  • 最新版本的 Xcode
  • 运行 macOS 的计算机,且满足所安装 Xcode 版本的系统要求

此 Codelab 是使用 Xcode 16.3 和 iOS 18.3 模拟器创建的。您应该使用最新版本的 Xcode 进行开发。

2. 创建新的 Xcode 项目

  1. 打开 Xcode,然后选择创建新的 Xcode 项目
  2. 选择 iOS 标签页,选择 App 模板,然后点击 Next

Xcode 项目创建模板页面

  1. 在项目选项中:
    • 输入您的商品名称
    • (可选)选择您的团队
    • 输入您的组织标识符
    • 记下生成的软件包标识符。以便稍后使用。
    • 对于接口,请选择以下任一选项:
      • 基于 SwiftUI 的应用的 SwiftUI
      • 基于 UIKit 的应用的故事板
    • 语言选择 Swift
    • 点击下一步,然后选择一个位置来保存项目。

Xcode 项目选项页面

3. 创建 OAuth 客户端

如需允许您的应用与 Google 的身份验证服务通信,您需要创建 OAuth 客户端 ID。这需要一个 Google Cloud 项目。以下步骤将引导您完成创建项目和 OAuth 客户端 ID 的整个过程。

选择或创建 Google Cloud 项目

  1. 前往 Google Cloud 控制台,然后选择或创建项目。如果您选择的是现有项目,控制台会自动引导您完成下一个必需的步骤。

Google Cloud 控制台项目选择器页面

  1. 为新的 Google Cloud 项目输入名称。
  2. 选择创建

Google Cloud 控制台项目选择器页面

如果您已为所选项目配置同意屏幕,系统不会提示您立即进行配置。在这种情况下,您可以跳过本部分,直接前往创建 OAuth 2.0 客户端

  1. 选择配置同意屏幕

Google Cloud 控制台创建 OAuth 客户端页面,其中显示了配置同意屏幕的要求

  1. 在“品牌”页面上,选择开始

Google Cloud 控制台品牌推广“开始”页面

  1. 在项目配置页面上,填写以下字段:
    • 应用信息:输入应用的名称和用户支持电子邮件地址。此支持电子邮件地址将公开显示,以便用户针对其同意问题与您联系。
    • 受众群体:选择外部
    • 联系信息:输入一个电子邮件地址,以便 Google 就您的项目与您联系。
    • 查看 Google API 服务:用户数据政策
    • 点击创建

Google Cloud 控制台客户端品牌配置页面

  1. 在导航菜单中选择客户页面。
  2. 点击创建客户端

Google Cloud 项目客户端页面

创建 OAuth 2.0 客户端

  1. 应用类型选择 iOS
  2. 输入客户的名称。
  3. 输入在上一步中创建的软件包标识符
  4. 输入 Apple 分配给您团队的团队 ID。此步骤目前是可选的,但若要在本 Codelab 的后续步骤中启用 App Check,则必须提供团队 ID。
  5. 选择创建

OAuth 客户端详细信息输入页面

  1. 从对话框窗口中复制客户端 ID,您稍后会用到它。
  2. 下载 plist 文件以供日后参考。

“OAuth 客户端 ID 已创建”对话框

4. 配置 Xcode 项目

下一步是设置 Xcode 项目,以便使用“通过 Google 账号登录”SDK。此过程包括将 SDK 作为依赖项添加到项目中,以及使用唯一的客户端 ID 配置项目设置。此 ID 可让 SDK 在登录过程中与 Google 的身份验证服务安全地通信。

安装“使用 Google 账号登录”依赖项

  1. 打开您的 Xcode 项目。
  2. 依次前往 File(文件)> Add Package Dependencies(添加软件包依赖项)
  3. 在搜索栏中,输入“通过 Google 账号登录”代码库的网址:https://github.com/google/GoogleSignIn-iOS

在 Swift Package Manager 中查找“使用 Google 账号登录”依赖项

  1. 选择添加软件包
  2. GoogleSignIn 软件包选择主要应用目标。
  3. 如果您使用的是 SwiftUI,请为 GoogleSignInSwift 软件包选择主要应用目标。如果您计划使用 UIKit,请勿为此软件包选择目标平台。
  4. 选择添加软件包

向您的项目添加“使用 Google 账号登录”依赖项

配置应用的凭据

  1. 在项目导航器中,点击项目的根目录。
  2. 在主编辑器区域中,从 TARGETS(目标)列表中选择您的主要应用目标。
  3. 选择编辑器区域顶部的信息标签页。
  4. 将鼠标悬停在自定义 iOS 目标属性部分中的最后一行上,然后点击显示的 + 按钮。

向 iOS 目标属性添加新的目标键

  1. 列中,输入 GIDClientID
  2. 列中,粘贴您从 Google Cloud 控制台中复制的客户端 ID。

将 GIDClientID 添加到主应用目标

  1. 打开从 Google Cloud 控制台下载的 plist 文件。
  2. 复制反向客户端 ID 的值。

Google Cloud 控制台 plist 文件

  1. 展开信息标签页底部的网址类型
  2. 选择 + 按钮。
  3. 网址方案框中输入倒序客户端 ID

向主应用目标添加 网址Schemes 键

现在,我们已准备好开始向应用添加登录按钮了!

5. 添加登录按钮

配置好 Xcode 项目后,就可以开始向应用添加“使用 Google 账号登录”按钮了!

此步骤的核心逻辑是对 GIDSignIn.sharedInstance.signIn 的调用。此方法会启动身份验证流程,并将控制权交给“使用 Google 账号登录”SDK,以便向用户显示“使用 Google 账号登录”流程。

SwiftUI

  1. 在 Xcode 项目导航器中找到 ContentView.swift 文件。
  2. 将此文件的内容替换为以下文本:
import GoogleSignIn
import GoogleSignInSwift
import SwiftUI

struct ContentView: View {
  var body: some View {
    VStack {
      GoogleSignInButton(action: handleSignInButton).padding()
    }
  }

  func handleSignInButton() {
    // Find the current window scene.
    guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
      print("There is no active window scene")
      return
    }

    // Get the root view controller from the window scene.
    guard
      let rootViewController = windowScene.windows.first(where: { $0.isKeyWindow })?
        .rootViewController
    else {
      print("There is no key window or root view controller")
      return
    }

    // Start the sign-in process.
    GIDSignIn.sharedInstance.signIn(
      withPresenting: rootViewController
    ) { signInResult, error in
      guard let result = signInResult else {
        // Inspect error
        print("Error signing in: \(error?.localizedDescription ?? "No error description")")
        return
      }
      // If sign in succeeded, display the app's main content View.
      print("ID Token: \(result.user.idToken?.tokenString ?? "")")
    }
  }
}

#Preview {
  ContentView()
}

iOS 模拟器上的 SwiftUI 框架“使用 Google 账号登录”按钮

UIKit

  1. 在 Xcode 项目导航器中找到 ViewController.swift 文件。
  2. 将此文件的内容替换为以下文本:
import GoogleSignIn
import UIKit

class ViewController: UIViewController {

  // Create an instance of the Sign in with Google button
  let signInButton = GIDSignInButton()

  override func viewDidLoad() {
    super.viewDidLoad()

    // Add the sign-in button to your view
    view.addSubview(signInButton)

    // Position the button using constraints
    signInButton.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
      signInButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      signInButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    ])

    // Add a target to the button to call a method when it's pressed
    signInButton.addTarget(self, action: #selector(signInButtonTapped), for: .touchUpInside)
  }

  // This method is called when the sign-in button is pressed.
  @objc func signInButtonTapped() {
    // Start the sign-in process.
    GIDSignIn.sharedInstance.signIn(withPresenting: self) { signInResult, error in
      guard let result = signInResult else {
        // Inspect error
        print("Error signing in: \(error?.localizedDescription ?? "No error description")")
        return
      }

      // If sign in succeeded, print the ID token.
      print("ID Token: \(result.user.idToken?.tokenString ?? "")")
    }
  }
}

iOS 模拟器上的 UIKit 框架“使用 Google 账号登录”按钮

查看登录按钮

在模拟器中启动应用。您会看到“使用 Google 账号登录”按钮,但该按钮暂时还无法正常使用。这是预期行为,因为您仍需实现代码来处理用户完成身份验证后重定向回您的应用的操作。

6. 自定义登录按钮

您可以自定义默认的“使用 Google 账号登录”按钮,使其更贴合应用的主题。借助 Google 登录 SDK,您可以修改按钮的配色方案和样式。

SwiftUI

以下这行代码会将默认按钮添加到网页中:

GoogleSignInButton(action: handleSignInButton)

通过向 GoogleSignInButton 的初始化函数传递参数,可以自定义 GoogleSignInButton。以下代码将使登录按钮在深色模式下显示。

  1. 打开 ContentView.swift
  2. 更新 GoogleSignInButton 的初始化程序,使其包含以下值:
GoogleSignInButton(
  scheme: .dark,  // Options: .light, .dark, .auto
  style: .standard,  // Options: .standard, .wide, .icon
  state: .normal,  // Options: .normal, .disabled
  action: handleSignInButton
).padding()

iOS 模拟器上的 SwiftUI 框架深色模式“使用 Google 账号登录”按钮

如需详细了解自定义选项,请参阅 GoogleSignInSwift 框架参考文档

UIKit

默认按钮是通过以下代码行创建的:

// Create an instance of the Sign in with Google button
let signInButton = GIDSignInButton()

// Add the button to your view
view.addSubview(signInButton)

通过在按钮实例上设置属性来自定义 GIDSignInButton。以下代码将使登录按钮在深色模式下显示。

  1. 打开 ViewController.swift
  2. viewDidLoad 函数中向视图添加登录按钮之前,立即添加以下代码行:
// Set the width and color of the sign-in button
signInButton.style = .standard  // Options: .standard, .wide, .iconOnly
signInButton.colorScheme = .dark  // Options: .dark, .light

iOS 模拟器上的 UIKit 框架深色模式“使用 Google 账号登录”按钮

如需详细了解自定义,请参阅 GoogleSignIn 框架参考

7. 处理身份验证重定向网址

添加登录按钮后,下一步是处理用户通过身份验证后发生的重定向。身份验证完成后,Google 会返回一个包含临时授权代码的网址。为了完成登录流程,处理程序会拦截此网址,并将其传递给 Google 登录 SDK,以换取已签名的 ID 令牌 (JWT)。

SwiftUI

  1. 打开包含 App 结构体的文件。此文件的命名方式取决于您的项目,因此名称类似于 YourProjectNameApp.swift
  2. 将此文件的内容替换为以下文本:
import GoogleSignIn
import SwiftUI

@main
struct iOS_Sign_in_with_Google_App: App {
  var body: some Scene {
    WindowGroup {
      ContentView()

        .onOpenURL { url in
          GIDSignIn.sharedInstance.handle(url)
        }
    }
  }
}

UIKit

  1. 打开 AppDelegate.swift
  2. 将以下 import 添加到文件顶部:
import GoogleSignIn
  1. AppDelegate 类中添加以下身份验证处理程序函数。建议将其放置在 application(_:didFinishLaunchingWithOptions:) 方法的右大括号后面:
func application(
  _ app: UIApplication,
  open url: URL,
  options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
  var handled: Bool

  handled = GIDSignIn.sharedInstance.handle(url)
  if handled {
    return true
  }
  // If not handled by this app, return false.
  return false
}

完成这些更改后,您的 AppDelegate.swift 文件应如下所示:

import GoogleSignIn
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    // Override point for customization after application launch.
    return true
  }

  func application(
    _ app: UIApplication,
    open url: URL,
    options: [UIApplication.OpenURLOptionsKey: Any] = [:]
  ) -> Bool {
    var handled: Bool

    handled = GIDSignIn.sharedInstance.handle(url)
    if handled {
      return true
    }
    // If not handled by this app, return false.
    return false
  }

  // MARK: UISceneSession Lifecycle

  func application(
    _ application: UIApplication,
    configurationForConnecting connectingSceneSession: UISceneSession,
    options: UIScene.ConnectionOptions
  ) -> UISceneConfiguration {
    // Called when a new scene session is being created.
    // Use this method to select a configuration to create the new scene with.
    return UISceneConfiguration(
      name: "Default Configuration",
      sessionRole: connectingSceneSession.role
    )
  }

  func application(
    _ application: UIApplication,
    didDiscardSceneSessions sceneSessions: Set<UISceneSession>
  ) {
    // Called when the user discards a scene session.
    // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
    // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
  }
}

测试登录流程

现在,您可以测试完整的登录流程了!

运行应用,然后点按登录按钮。完成身份验证后,Google 会显示一个意见征求界面,您可以在其中授予应用访问您信息的权限。您批准后,登录流程将完成,系统会将您返回到相应应用。

登录流程成功完成后,“使用 Google 账号登录”SDK 会将用户的凭据安全地存储在设备的钥匙串中。这些凭据稍后可用于允许用户在后续应用启动时保持登录状态。

8. 添加退出按钮

现在,登录功能已正常运行,下一步是添加退出按钮并更新界面,以反映用户的当前登录状态。登录成功后,SDK 会提供一个 GIDGoogleUser 对象。此对象包含一个 profile 属性,其中包含用户的姓名和电子邮件地址等基本信息,您将使用这些信息来个性化界面。

SwiftUI

  1. 打开 ContentView.swift 文件。
  2. ContentView 结构的顶部添加一个状态变量。此变量将在用户登录后保存其信息。由于它是 @State 变量,因此每当其值发生变化时,SwiftUI 都会自动更新界面:
struct ContentView: View {
  @State private var user: GIDGoogleUser?
}
  1. ContentView 结构的当前 body 替换为以下 VStack。这将检查 user 状态变量是否包含用户。如果已登录,系统会显示欢迎消息和退出按钮。如果未显示,则会显示原来的“使用 Google 账号登录”按钮:
var body: some View {
  VStack {
    // Check if the user is signed in.
    if let user = user {
      // If signed in, show a welcome message and the sign-out button.
      Text("Hello, \(user.profile?.givenName ?? "User")!")
        .font(.title)
        .padding()

      Button("Sign Out", action: signOut)
        .buttonStyle(.borderedProminent)

    } else {
      // If not signed in, show the "Sign in with Google" button.
      GoogleSignInButton(
        scheme: .dark,  // Options: .light, .dark, .auto
        style: .standard,  // Options: .standard, .wide, .icon
        state: .normal,  // Options: .normal, .disabled
        action: handleSignInButton
      ).padding()
    }
  }
}
  1. 更新 handleSignInButton 完成代码块,以将 signInResult.user 分配给新的 user 变量。这会触发界面切换到已登录视图:
func handleSignInButton() {
  // Find the current window scene.
  guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
    print("There is no active window scene")
    return
  }

  // Get the root view controller from the window scene.
  guard
    let rootViewController = windowScene.windows.first(where: { $0.isKeyWindow })?
      .rootViewController
  else {
    print("There is no key window or root view controller")
    return
  }

  // Start the sign-in process.
  GIDSignIn.sharedInstance.signIn(
    withPresenting: rootViewController
  ) { signInResult, error in
    guard let result = signInResult else {
      // Inspect error
      print("Error signing in: \(error?.localizedDescription ?? "No error description")")
      return
    }

    DispatchQueue.main.async {
      self.user = result.user
    }

    // If sign in succeeded, display the app's main content View.
    print("ID Token: \(result.user.idToken?.tokenString ?? "")")
  }
}
  1. ContentView 结构的底部添加一个由退出登录按钮调用的新 signOut 函数:
func signOut() {
  GIDSignIn.sharedInstance.signOut()
  // After signing out, set the `user` state variable to `nil`.
  self.user = nil
}

启动应用并登录。成功进行身份验证后,您应该会看到界面发生变化!

iOS 模拟器上的 SwiftUI 框架登录状态

完成这些更改后,您的 ContentView.swift 文件应如下所示:

import GoogleSignIn
import GoogleSignInSwift
import SwiftUI

struct ContentView: View {

  @State private var user: GIDGoogleUser?

  var body: some View {
    VStack {
      // Check if the user is signed in.
      if let user = user {
        // If signed in, show a welcome message and the sign-out button.
        Text("Hello, \(user.profile?.givenName ?? "User")!")
          .font(.title)
          .padding()

        Button("Sign Out", action: signOut)
          .buttonStyle(.borderedProminent)

      } else {
        // If not signed in, show the "Sign in with Google" button.
        GoogleSignInButton(
          scheme: .dark,  // Options: .light, .dark, .auto
          style: .standard,  // Options: .standard, .wide, .icon
          state: .normal,  // Options: .normal, .disabled
          action: handleSignInButton
        ).padding()
      }
    }
  }

  func handleSignInButton() {
    // Find the current window scene.
    guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
      print("There is no active window scene")
      return
    }

    // Get the root view controller from the window scene.
    guard
      let rootViewController = windowScene.windows.first(where: { $0.isKeyWindow })?
        .rootViewController
    else {
      print("There is no key window or root view controller")
      return
    }

    // Start the sign-in process.
    GIDSignIn.sharedInstance.signIn(
      withPresenting: rootViewController
    ) { signInResult, error in
      guard let result = signInResult else {
        // Inspect error
        print("Error signing in: \(error?.localizedDescription ?? "No error description")")
        return
      }

      DispatchQueue.main.async {
        self.user = result.user
      }

      // If sign in succeeded, display the app's main content View.
      print("ID Token: \(result.user.idToken?.tokenString ?? "")")
    }
  }

  func signOut() {
    GIDSignIn.sharedInstance.signOut()
    // After signing out, set the `user` state variable to `nil`.
    self.user = nil
  }
}

#Preview {
  ContentView()
}

UIKit

  1. 打开 ViewController.swift
  2. ViewController 的顶部,紧挨着您声明 signInButton 的位置下方,添加一个退出按钮和一个欢迎标签:
let signOutButton = UIButton(type: .system)
let welcomeLabel = UILabel()
  1. 将以下函数添加到 ViewController 的底部。此函数会根据用户的登录状态向其显示不同的界面:
private func updateUI(for user: GIDGoogleUser?) {
  if let user = user {
    // User is signed in.
    signInButton.isHidden = true
    signOutButton.isHidden = false
    welcomeLabel.isHidden = false
    welcomeLabel.text = "Hello, \(user.profile?.givenName ?? "User")!"
  } else {
    // User is signed out.
    signInButton.isHidden = false
    signOutButton.isHidden = true
    welcomeLabel.isHidden = true
  }
}
  1. viewDidLoad 函数的底部,添加以下代码以将欢迎标签和退出按钮添加到视图中:
// --- Set up the Welcome Label ---
welcomeLabel.translatesAutoresizingMaskIntoConstraints = false
welcomeLabel.textAlignment = .center
welcomeLabel.font = .systemFont(ofSize: 24, weight: .bold)
view.addSubview(welcomeLabel)

NSLayoutConstraint.activate([
  welcomeLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
  welcomeLabel.bottomAnchor.constraint(equalTo: signInButton.topAnchor, constant: -20),
])

// --- Set up the Sign-Out Button ---
signOutButton.translatesAutoresizingMaskIntoConstraints = false
signOutButton.setTitle("Sign Out", for: .normal)
view.addSubview(signOutButton)

NSLayoutConstraint.activate([
  signOutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
  signOutButton.topAnchor.constraint(equalTo: signInButton.bottomAnchor, constant: 20),
])

signOutButton.addTarget(self, action: #selector(signOutButtonTapped), for: .touchUpInside)

// --- Set Initial UI State ---
updateUI(for: nil)
  1. 更新 signInButtonTapped 函数,以便在成功登录后调用 UpdateUI 方法:
@objc func signInButtonTapped() {
  // Start the sign-in process.
  GIDSignIn.sharedInstance.signIn(withPresenting: self) { signInResult, error in
    guard let result = signInResult else {
      // Inspect error
      print("Error signing in: \(error?.localizedDescription ?? "No error description")")
      return
    }

    // If sign in succeeded, print the ID token.
    print("ID Token: \(result.user.idToken?.tokenString ?? "")")

    DispatchQueue.main.async {
      self.updateUI(for: result.user)
    }
  }
}
  1. 最后,向 ViewController 添加一个 signOutButtonTapped 函数,以处理退出流程:
@objc func signOutButtonTapped() {
  GIDSignIn.sharedInstance.signOut()
  // Update the UI for the signed-out state.
  updateUI(for: nil)
}

启动应用并登录。成功进行身份验证后,您应该会看到界面发生变化!

iOS 模拟器上的 UIKit 框架登录状态

完成这些更改后,您的 ViewController.swift 文件应如下所示:

import GoogleSignIn
import UIKit

class ViewController: UIViewController {

  // Create an instance of the Sign in with Google button
  let signInButton = GIDSignInButton()
  let signOutButton = UIButton(type: .system)
  let welcomeLabel = UILabel()

  override func viewDidLoad() {
    super.viewDidLoad()

    // Set the width and color of the sign-in button
    signInButton.style = .standard  // Options: .standard, .wide, .iconOnly
    signInButton.colorScheme = .dark  // Options: .dark, .light

    // Add the sign-in button to your view
    view.addSubview(signInButton)

    // Position the button using constraints
    signInButton.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
      signInButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      signInButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    ])

    // Add a target to the button to call a method when it's pressed
    signInButton.addTarget(self, action: #selector(signInButtonTapped), for: .touchUpInside)

    // --- Set up the Welcome Label ---
    welcomeLabel.translatesAutoresizingMaskIntoConstraints = false
    welcomeLabel.textAlignment = .center
    welcomeLabel.font = .systemFont(ofSize: 24, weight: .bold)
    view.addSubview(welcomeLabel)

    NSLayoutConstraint.activate([
      welcomeLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      welcomeLabel.bottomAnchor.constraint(equalTo: signInButton.topAnchor, constant: -20),
    ])

    // --- Set up the Sign-Out Button ---
    signOutButton.translatesAutoresizingMaskIntoConstraints = false
    signOutButton.setTitle("Sign Out", for: .normal)
    view.addSubview(signOutButton)

    NSLayoutConstraint.activate([
      signOutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      signOutButton.topAnchor.constraint(equalTo: signInButton.bottomAnchor, constant: 20),
    ])

    signOutButton.addTarget(self, action: #selector(signOutButtonTapped), for: .touchUpInside)

    // --- Set Initial UI State ---
    updateUI(for: nil)
  }

  // This method is called when the sign-in button is pressed.
  @objc func signInButtonTapped() {
    // Start the sign-in process.
    GIDSignIn.sharedInstance.signIn(withPresenting: self) { signInResult, error in
      guard let result = signInResult else {
        // Inspect error
        print("Error signing in: \(error?.localizedDescription ?? "No error description")")
        return
      }

      // If sign in succeeded, print the ID token.
      print("ID Token: \(result.user.idToken?.tokenString ?? "")")

      DispatchQueue.main.async {
        self.updateUI(for: result.user)
      }
    }
  }

  private func updateUI(for user: GIDGoogleUser?) {
    if let user = user {
      // User is signed in.
      signInButton.isHidden = true
      signOutButton.isHidden = false
      welcomeLabel.isHidden = false
      welcomeLabel.text = "Hello, \(user.profile?.givenName ?? "User")!"
    } else {
      // User is signed out.
      signInButton.isHidden = false
      signOutButton.isHidden = true
      welcomeLabel.isHidden = true
    }
  }

  @objc func signOutButtonTapped() {
    GIDSignIn.sharedInstance.signOut()
    // Update the UI for the signed-out state.
    updateUI(for: nil)
  }
}

9. 恢复用户的登录状态

为了改善回访用户的体验,下一步是在应用启动时恢复其登录状态。调用 restorePreviousSignIn 会使用保存在钥匙串中的凭据以静默方式重新登录用户,确保用户不必每次都完成登录流程。

SwiftUI

  1. 打开 ContentView.swift
  2. body 变量中的 VStack 后面直接添加以下代码:
.onAppear {
  // On appear, try to restore a previous sign-in.
  GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
    // This closure is called when the restoration is complete.
    if let user = user {
      // If a user was restored, update the `user` state variable.
      DispatchQueue.main.async {
        self.user = user
      }

      // Print the ID token to the console when restored.
      print("Restored ID Token: \(user.idToken?.tokenString ?? "")")
    }
  }
}

您的 ContentView.swift 应如下所示:

import GoogleSignIn
import GoogleSignInSwift
import SwiftUI

struct ContentView: View {

  @State private var user: GIDGoogleUser?

  var body: some View {
    VStack {
      // Check if the user is signed in.
      if let user = user {
        // If signed in, show a welcome message and the sign-out button.
        Text("Hello, \(user.profile?.givenName ?? "User")!")
          .font(.title)
          .padding()

        Button("Sign Out", action: signOut)
          .buttonStyle(.borderedProminent)

      } else {
        // If not signed in, show the "Sign in with Google" button.
        GoogleSignInButton(
          scheme: .dark,  // Options: .light, .dark, .auto
          style: .standard,  // Options: .standard, .wide, .icon
          state: .normal,  // Options: .normal, .disabled
          action: handleSignInButton
        ).padding()
      }
    }

    .onAppear {
      // On appear, try to restore a previous sign-in.
      GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
        // This closure is called when the restoration is complete.
        if let user = user {
          // If a user was restored, update the `user` state variable.
          DispatchQueue.main.async {
            self.user = user
          }

          // Print the ID token to the console when restored.
          print("Restored ID Token: \(user.idToken?.tokenString ?? "")")
        }
      }
    }
  }

  func handleSignInButton() {
    // Find the current window scene.
    guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
      print("There is no active window scene")
      return
    }

    // Get the root view controller from the window scene.
    guard
      let rootViewController = windowScene.windows.first(where: { $0.isKeyWindow })?
        .rootViewController
    else {
      print("There is no key window or root view controller")
      return
    }

    // Start the sign-in process.
    GIDSignIn.sharedInstance.signIn(
      withPresenting: rootViewController
    ) { signInResult, error in
      guard let result = signInResult else {
        // Inspect error
        print("Error signing in: \(error?.localizedDescription ?? "No error description")")
        return
      }

      DispatchQueue.main.async {
        self.user = result.user
      }

      // If sign in succeeded, display the app's main content View.
      print("ID Token: \(result.user.idToken?.tokenString ?? "")")
    }
  }

  func signOut() {
    GIDSignIn.sharedInstance.signOut()
    // After signing out, set the `user` state variable to `nil`.
    self.user = nil
  }
}

#Preview {
  ContentView()
}

UIKit

  1. 打开 ViewController.swift
  2. viewDidLoad 方法的末尾添加以下 restorePreviousSignIn 调用:
// Attempt to restore a previous sign-in session
GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
  if let user = user {
    print("Successfully restored sign-in for user: \(user.profile?.givenName ?? "Unknown")")

    // Print the ID token when a session is restored.
    print("Restored ID Token: \(user.idToken?.tokenString ?? "")")

    // On success, update the UI for the signed-in state on the main thread.
    DispatchQueue.main.async {
      self.updateUI(for: user)
    }
  }
}

您的 ViewController.swift 文件应如下所示:

import GoogleSignIn
import UIKit

class ViewController: UIViewController {

  // Create an instance of the Sign in with Google button
  let signInButton = GIDSignInButton()
  let signOutButton = UIButton(type: .system)
  let welcomeLabel = UILabel()

  override func viewDidLoad() {
    super.viewDidLoad()

    // Set the width and color of the sign-in button
    signInButton.style = .standard  // Options: .standard, .wide, .iconOnly
    signInButton.colorScheme = .dark  // Options: .dark, .light

    // Add the sign-in button to your view
    view.addSubview(signInButton)

    // Position the button using constraints
    signInButton.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
      signInButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      signInButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
    ])

    // Add a target to the button to call a method when it's pressed
    signInButton.addTarget(self, action: #selector(signInButtonTapped), for: .touchUpInside)

    // --- Set up the Welcome Label ---
    welcomeLabel.translatesAutoresizingMaskIntoConstraints = false
    welcomeLabel.textAlignment = .center
    welcomeLabel.font = .systemFont(ofSize: 24, weight: .bold)
    view.addSubview(welcomeLabel)

    NSLayoutConstraint.activate([
      welcomeLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      welcomeLabel.bottomAnchor.constraint(equalTo: signInButton.topAnchor, constant: -20),
    ])

    // --- Set up the Sign-Out Button ---
    signOutButton.translatesAutoresizingMaskIntoConstraints = false
    signOutButton.setTitle("Sign Out", for: .normal)
    view.addSubview(signOutButton)

    NSLayoutConstraint.activate([
      signOutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
      signOutButton.topAnchor.constraint(equalTo: signInButton.bottomAnchor, constant: 20),
    ])

    signOutButton.addTarget(self, action: #selector(signOutButtonTapped), for: .touchUpInside)

    // --- Set Initial UI State ---
    updateUI(for: nil)

    // Attempt to restore a previous sign-in session
    GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
      if let user = user {
        print("Successfully restored sign-in for user: \(user.profile?.givenName ?? "Unknown")")

        // Print the ID token when a session is restored.
        print("Restored ID Token: \(user.idToken?.tokenString ?? "")")

        // On success, update the UI for the signed-in state on the main thread.
        DispatchQueue.main.async {
          self.updateUI(for: user)
        }
      }
    }
  }

  // This method is called when the sign-in button is pressed.
  @objc func signInButtonTapped() {
    // Start the sign-in process.
    GIDSignIn.sharedInstance.signIn(withPresenting: self) { signInResult, error in
      guard let result = signInResult else {
        // Inspect error
        print("Error signing in: \(error?.localizedDescription ?? "No error description")")
        return
      }

      // If sign in succeeded, print the ID token.
      print("ID Token: \(result.user.idToken?.tokenString ?? "")")

      DispatchQueue.main.async {
        self.updateUI(for: result.user)
      }
    }
  }

  private func updateUI(for user: GIDGoogleUser?) {
    if let user = user {
      // User is signed in.
      signInButton.isHidden = true
      signOutButton.isHidden = false
      welcomeLabel.isHidden = false
      welcomeLabel.text = "Hello, \(user.profile?.givenName ?? "User")!"
    } else {
      // User is signed out.
      signInButton.isHidden = false
      signOutButton.isHidden = true
      welcomeLabel.isHidden = true
    }
  }

  @objc func signOutButtonTapped() {
    GIDSignIn.sharedInstance.signOut()
    // Update the UI for the signed-out state.
    updateUI(for: nil)
  }
}

测试静默登录

登录后,完全退出该应用,然后重新启动。您应该会看到,您现在已自动登录,无需点按按钮。

10. 了解 ID 令牌

虽然 GIDGoogleUser 对象可用于使用用户的姓名和电子邮件地址来个性化界面,但从 SDK 返回的最重要的数据是 ID 令牌。

此 Codelab 使用在线工具来检查 JWT 内容。在正式版应用中,您应将此 ID 令牌发送到后端服务器。您的服务器必须验证 ID 令牌的完整性,并使用 JWT 执行更有意义的操作,例如在后端平台上创建新账号或为用户建立新会话。

访问并解码 JWT 令牌

  1. 启动应用。
  2. 打开 Xcode 控制台。您应该会看到输出的 ID 令牌。其格式应为 eyJhbGciOiJSUzI1Ni ... Hecz6Wm4Q
  3. 复制 ID 令牌,然后使用 jwt.io 等在线工具解码 JWT。

解码后的 JWT 将如下所示:

{
  "alg": "RS256",
  "kid": "c8ab71530972bba20b49f78a09c9852c43ff9118",
  "typ": "JWT"
}
{
  "iss": "https://accounts.google.com",
  "azp": "171291171076-rrbkcjrp5jbte92ai9gub115ertscphi.apps.googleusercontent.com",
  "aud": "171291171076-rrbkcjrp5jbte92ai9gub115ertscphi.apps.googleusercontent.com",
  "sub": "10769150350006150715113082367",
  "email": "example@example.com",
  "email_verified": true,
  "at_hash": "JyCYDmHtzhjkb0-qJhKsMg",
  "name": "Kimya",
  "picture": "https://lh3.googleusercontent.com/a/ACg8ocIyy4VoR31t_n0biPVcScBHwZOCRaKVDb_MoaMYep65fyqoAw=s96-c",
  "given_name": "Kimya",
  "iat": 1758645896,
  "exp": 1758649496
}

值得注意的令牌字段

解码后的 ID 令牌包含具有不同用途的字段。虽然有些信息(例如姓名和电子邮件地址)很容易理解,但其他信息则由后端服务器用于验证。

以下字段尤其需要了解:

  • subsub 字段是用户 Google 账号的唯一永久标识符。用户可以更改其主电子邮件地址或姓名,但其 sub ID 永远不会更改。因此,sub 字段非常适合用作后端用户账号的主键。

从 ID 令牌获取用户信息一文详细介绍了所有令牌字段的含义。

11. 使用 App Check 保护应用的安全

强烈建议您启用 App Check,以确保只有您的应用可以代表您的项目访问 Google 的 OAuth 2.0 端点。App Check 的工作原理是验证发送到后端服务的请求是否来自真实且未经篡改的设备上的正版应用。

本部分介绍了如何将 App Check 集成到应用中,并将其配置为既可在模拟器中进行调试,又可在真实设备上运行的正式版 build 中使用。

控制台设置

将 App Check 集成到应用中需要在 Google Cloud 控制台和 Firebase 控制台中进行一次性设置。这包括在 Google Cloud 控制台中为 iOS OAuth 客户端启用 App Check、创建用于 App Check 调试提供程序的 API 密钥,以及将 Google Cloud 项目关联到 Firebase。

在 Google Cloud 控制台中启用 App Check

  1. 前往与您的 Google Cloud 项目关联的客户端列表。
  2. 选择您为 iOS 应用创建的 OAuth 2.0 客户端 ID。
  3. iOS 版 Google Identity 下方,将 App Check 切换为开启状态

包含 App Check 开关的 OAuth 客户端修改页面

  1. 点击保存

创建 API 密钥

  1. 前往 Google Cloud 项目的 API 库页面。
  2. 在搜索栏中输入 Firebase App Check API

Google Cloud 控制台 API 库页面

  1. 选择并启用 Firebase App Check API
  2. 前往 API 和服务,然后在导航菜单中选择凭据
  3. 选择页面顶部的创建凭据

Google Cloud 控制台“API 凭据”页面

  1. 为此 API 密钥分配一个名称。
  2. 应用限制下,选择“iOS 应用”。
  3. 将应用的软件包标识符添加为获批应用。
  4. API 限制下,选择限制密钥
  5. 从下拉菜单中选择 Firebase App Check API
  6. 选择创建

Google Cloud Console API 密钥创建页面

  1. 复制创建的 API 密钥。后续步骤将会用到该地址。

将 Firebase 添加到您的 Google Cloud 项目

  1. 前往 Firebase 控制台
  2. 选择开始之前,请先设置 Firebase 项目
  3. 选择将 Firebase 添加到 Google Cloud 项目

将 Firebase 添加到现有 Google Cloud 项目

  1. 从下拉菜单中选择一个 Google Cloud 项目,然后继续完成注册流程。
  2. 选择添加 Firebase
  3. Firebase 项目准备就绪后,选择继续以打开项目。

客户端代码集成

在 Google Cloud 项目中配置 App Check 后,接下来需要编写客户端代码来启用它。在生产环境和调试环境中,用于证明的提供程序不同。真实设备上的正式版应用使用 Apple 的内置 App Attest 服务来证明其真实性。不过,由于 iOS 模拟器无法提供此类认证,因此调试环境需要一个特殊的调试提供程序,该提供程序会传递 API 密钥。

以下代码使用编译器指令在 build 时自动选择正确的提供程序,从而处理这两种情况。

SwiftUI

  1. 打开主应用文件。
  2. 在导入部分之后和 @main 属性之前定义以下 AppDelegate 类:
class AppDelegate: NSObject, UIApplicationDelegate {
  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {

    #if targetEnvironment(simulator)
      // Configure for debugging on a simulator.
      // TODO: Replace "YOUR_API_KEY" with the key from your Google Cloud project.
      let apiKey = "YOUR_API_KEY"
      GIDSignIn.sharedInstance.configureDebugProvider(withAPIKey: apiKey) { error in
        if let error {
          print("Error configuring GIDSignIn debug provider: \(error)")
        }
      }
    #else
      // Configure GIDSignIn for App Check on a real device.
      GIDSignIn.sharedInstance.configure { error in
        if let error {
          print("Error configuring GIDSignIn for App Check: \(error)")
        } else {
          print("GIDSignIn configured for App Check.")
        }
      }
    #endif

    return true
  }
}
  1. 将所提供代码中的 "YOUR_API_KEY" 替换为您从 Google Cloud 控制台中复制的 API 密钥。
  2. App 结构体中,紧挨着 body 变量之前添加以下代码行。这会将您的 AppDelegate 类注册到应用生命周期,使其能够响应应用启动和其他系统事件:
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

您的主应用文件应如下所示:

import GoogleSignIn
import SwiftUI

class AppDelegate: NSObject, UIApplicationDelegate {
  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {

    #if targetEnvironment(simulator)
      // Configure for debugging on a simulator.
      // TODO: Replace "YOUR_API_KEY" with the key from your Google Cloud project.
      let apiKey = "YOUR_API_KEY"
      GIDSignIn.sharedInstance.configureDebugProvider(withAPIKey: apiKey) { error in
        if let error {
          print("Error configuring GIDSignIn debug provider: \(error)")
        }
      }
    #else
      // Configure GIDSignIn for App Check on a real device.
      GIDSignIn.sharedInstance.configure { error in
        if let error {
          print("Error configuring GIDSignIn for App Check: \(error)")
        } else {
          print("GIDSignIn configured for App Check.")
        }
      }
    #endif

    return true
  }
}

@main
struct iOS_Sign_in_with_Google_App: App {

  @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

  var body: some Scene {
    WindowGroup {
      ContentView()

        .onOpenURL { url in
          GIDSignIn.sharedInstance.handle(url)
        }
    }
  }
}

UIKit

  1. 打开 AppDelegate.swift
  2. 更新 application(_:didFinishLaunchingWithOptions:) 方法以包含 App Check 初始化:
func application(
  _ application: UIApplication,
  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {

  #if targetEnvironment(simulator)
    // Configure for debugging on a simulator.
    // TODO: Replace "YOUR_API_KEY" with the key from your Google Cloud project.
    let apiKey = "YOUR_API_KEY"
    GIDSignIn.sharedInstance.configureDebugProvider(withAPIKey: apiKey) { error in
      if let error {
        print("Error configuring GIDSignIn debug provider: \(error)")
      }
    }
  #else
    // Configure GIDSignIn for App Check on a real device.
    GIDSignIn.sharedInstance.configure { error in
      if let error {
        print("Error configuring GIDSignIn for App Check: \(error)")
      }
    }
  #endif

  return true
}
  1. 将所提供代码中的 "YOUR_API_KEY" 替换为您从 Google Cloud 控制台中复制的 API 密钥。

您的 AppDelegate.swift 文件应如下所示:

import GoogleSignIn
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {

    #if targetEnvironment(simulator)
      // Configure for debugging on a simulator.
      // TODO: Replace "YOUR_API_KEY" with the key from your Google Cloud project.
      let apiKey = "YOUR_API_KEY"
      GIDSignIn.sharedInstance.configureDebugProvider(withAPIKey: apiKey) { error in
        if let error {
          print("Error configuring GIDSignIn debug provider: \(error)")
        }
      }
    #else
      // Configure GIDSignIn for App Check on a real device.
      GIDSignIn.sharedInstance.configure { error in
        if let error {
          print("Error configuring GIDSignIn for App Check: \(error)")
        }
      }
    #endif

    return true
  }

  func application(
    _ app: UIApplication,
    open url: URL,
    options: [UIApplication.OpenURLOptionsKey: Any] = [:]
  ) -> Bool {
    var handled: Bool

    handled = GIDSignIn.sharedInstance.handle(url)
    if handled {
      return true
    }
    // If not handled by this app, return false.
    return false
  }

  // MARK: UISceneSession Lifecycle

  func application(
    _ application: UIApplication,
    configurationForConnecting connectingSceneSession: UISceneSession,
    options: UIScene.ConnectionOptions
  ) -> UISceneConfiguration {
    // Called when a new scene session is being created.
    // Use this method to select a configuration to create the new scene with.
    return UISceneConfiguration(
      name: "Default Configuration",
      sessionRole: connectingSceneSession.role
    )
  }

  func application(
    _ application: UIApplication,
    didDiscardSceneSessions sceneSessions: Set<UISceneSession>
  ) {
    // Called when the user discards a scene session.
    // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
    // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
  }
}

在模拟器上测试 App Check

  1. 在 Xcode 菜单栏中,依次前往 Product > Scheme > Edit Scheme
  2. 在导航菜单中选择运行
  3. 选择 Arguments 标签页。
  4. Arguments Passed on Launch 部分中,选择 + 并添加 -FIRDebugEnabled。此启动实参可启用 Firebase 调试日志记录。
  5. 选择关闭

Xcode 实参编辑器页面

  1. 在模拟器上启动应用。
  2. 复制在 Xcode 控制台中输出的 App Check 调试令牌

Xcode 控制台中的 App Check 调试令牌

  1. 前往 Firebase 控制台中的项目。
  2. 展开导航菜单中的构建部分。
  3. 选择应用检查
  4. 选择应用标签页。
  5. 将鼠标悬停在应用上,然后选择三点状菜单图标。

Firebase App Check 设置

  1. 选择管理调试令牌
  2. 选择添加调试令牌
  3. 为调试令牌指定一个名称,然后粘贴您之前复制的调试令牌作为值。
  4. 选择保存以注册您的令牌。

Firebase App Check 调试令牌管理

  1. 返回模拟器并登录。

指标可能需要几分钟才能显示在控制台中。在他们这样做后,您可以在以下两个位置查看已验证请求是否有所增加,以确认应用检查是否正常运行:

  • 在 Firebase 控制台的“App Check”部分中,位于“API”标签页下。

Firebase App Check 指标

  • 在 Google Cloud 控制台中,进入 OAuth 客户端的修改页面。

Google Cloud Console App Check 指标

在监控应用的 App Check 指标并确认合法请求已通过验证后,您应启用 App Check 强制执行。强制执行后,App Check 会拒绝所有未经验证的请求,确保只有来自您的正版应用的流量才能代表您的项目访问 Google 的 OAuth 2.0 端点。

12. 其他资源

恭喜!

您已配置 OAuth 2.0 iOS 客户端,向 iOS 应用添加了“使用 Google 账号登录”按钮,了解了如何自定义按钮的外观,解码了 JWT ID 令牌,并为应用启用了 App Check。

以下链接可能有助于您了解后续步骤:

常见问题解答