1. 准备工作
在此 Codelab 中,您将学习如何使用 Credential Manager 在 Android 上实现“使用 Google 账号登录”。
前提条件
学习内容
- 如何创建 Google Cloud 项目
- 如何在 Google Cloud 控制台中创建 OAuth 客户端
- 如何使用底部工作表的流程实现“使用 Google 账号登录”功能
- 如何使用按钮流程实现“使用 Google 账号登录”功能
所需条件
2. 创建 Android Studio 项目
时长 3:00 - 5:00
首先,我们需要在 Android Studio 中创建一个新项目:
- 打开 Android Studio
- 点击新建项目
- 选择 Phone and Tablet 和 Empty Activity
- 点击下一步。
- 现在,您可以设置项目的几个部分了:
- 名称:这是项目的名称
- 软件包名称:系统会根据您的项目名称自动填充此字段
- 保存位置:此位置应默认为 Android Studio 保存项目的文件夹。您可以将其更改为任意位置。
- Minimum SDK:这是您的应用构建时所依据的最低 Android SDK 版本。在此 Codelab 中,我们将使用 API 36 (Baklava)
- 点击 Finish。
- Android Studio 将创建项目并下载基本应用所需的所有依赖项,这可能需要几分钟时间。如需查看此过程,只需点击 build 图标:
- 完成后,Android Studio 应如下所示:
3. 设置 Google Cloud 项目
创建 Google Cloud 项目
- 前往 Google Cloud 控制台
- 打开项目或创建新项目
- 点击 API 和服务图标
- 前往 OAuth 权限请求页面
- 您必须填写概览中的字段才能继续。点击开始,开始填写以下信息:
- 应用名称:此应用的名称,应与您在 Android Studio 中创建项目时使用的名称相同
- 用户支持电子邮件地址:此页面会显示您登录时使用的 Google 账号以及您管理的任何 Google 群组
- 受众群体:
- 内部:仅在组织内使用的应用。如果您没有与 Google Cloud 项目关联的组织,则无法选择此选项。
- 我们将使用“外部”。
- 联系信息:可以是您希望作为应用联系人的任何电子邮件地址
- 查看 Google API 服务:用户数据政策。
- 查看并同意用户数据政策后,点击创建图标
设置 OAuth 客户端
现在,我们已经设置了 Google Cloud 项目,接下来需要添加 Web 客户端和 Android 客户端,以便我们可以使用其客户端 ID 向 OAuth 后端服务器发出 API 调用。
对于 Android Web 客户端,您需要:
- 应用的软件包名称(例如 com.example.example)
- 应用的 SHA-1 签名
- 什么是 SHA-1 签名?
- SHA-1 指纹是根据应用的签名密钥生成的加密哈希。它实际上是您的特定应用的签名证书的唯一标识符。您可以将其视为应用的数字“签名”。
- 为什么需要 SHA-1 签名?
- SHA-1 指纹可确保只有使用特定签名密钥签名的应用才能使用您的 OAuth 2.0 客户端 ID 请求访问令牌,从而防止其他应用(即使是具有相同软件包名称的应用)访问您项目的资源和用户数据。
- 可以这样理解:
- 应用的签名密钥就像应用“门”的实体钥匙。它允许访问应用的内部运作方式。
- SHA-1 指纹就像与实体钥匙关联的唯一钥匙卡 ID。它是用于标识特定密钥的特定代码。
- OAuth 2.0 客户端 ID 就像是特定 Google 资源或服务(例如 Google 登录)的入口代码。
- 在 OAuth 客户端设置期间提供 SHA-1 指纹时,您实际上是在告诉 Google:“只有具有此特定 ID (SHA-1) 的房卡才能打开此访问代码(客户端 ID)。”这样可确保只有您的应用可以访问与该验证码关联的 Google 服务。”
- 什么是 SHA-1 签名?
对于 Web 客户端,我们只需要您想在控制台中用于标识客户端的名称。
创建 Android OAuth 2.0 客户端
- 前往客户页面
- 点击创建客户端图标
- 为“应用类型”选择 Android
- 您需要指定应用的软件包名称
- 在 Android Studio 中,我们需要获取应用的 SHA-1 签名,并将其复制/粘贴到此处:
- 前往 Android Studio 并打开终端
- 运行以下命令:
此命令旨在列出密钥库中特定条目(别名)的详细信息。keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
-list
:此选项用于告知 keytool 列出密钥库的内容。-v
:此选项可启用详细输出,提供有关条目的更详细信息。-keystore ~/.android/debug.keystore
:用于指定密钥库文件的路径。-alias androiddebugkey
:用于指定要检查的密钥的别名(条目名称)。-storepass android
:用于提供密钥库文件的密码。-keypass android
:这会提供指定别名的私钥的密码。
- 复制 SHA-1 签名的值:
- 返回 Google Cloud 窗口,然后粘贴 SHA-1 签名值:
- 现在,您的屏幕应类似于以下内容,您可以点击创建:
创建 Web OAuth 2.0 客户端
- 如需创建 Web 应用客户端 ID,请重复创建 Android 客户端 部分中的第 1-2 步,然后为应用类型选择 Web 应用
- 为客户端命名(这将是 OAuth 客户端):
- 点击创建图标
- 请继续从弹出式窗口中复制客户端 ID,您稍后会用到它
现在,我们已设置好所有 OAuth 客户端,可以返回到 Android Studio,制作我们的“使用 Google 账号登录”Android 应用了!
4. 设置 Android 虚拟设备
如需在没有实体 Android 设备的情况下快速测试应用,您需要创建一个 Android 虚拟设备,以便在 Android Studio 中构建应用并立即运行。如果您想使用实体 Android 设备进行测试,可以按照 Android 开发者文档中的说明操作
创建 Android 虚拟设备
- 在 Android Studio 中,打开设备管理器
- 依次点击“+”按钮 >“创建虚拟设备”图标
- 您可以在此处添加项目所需的任何设备。在此 Codelab 中,选择 Medium Phone,然后点击 Next
- 现在,您可以为项目配置设备,方法是为设备指定唯一名称、选择设备将运行的 Android 版本等。确保将 API 设置为 API 36“Baklava”;Android 16,然后点击 Finish
- 您应该会在设备管理器中看到新设备。如需验证设备是否正常运行,请点击刚刚创建的设备旁边的
- 设备现在应该正在运行!
登录 Android 虚拟设备
您刚刚创建的设备可以正常运行,现在为了防止在测试“使用 Google 账号登录”功能时出现错误,我们需要使用 Google 账号登录该设备。
- 前往“设置”:
- 点击虚拟设备屏幕中央,然后向上滑动
- 找到“设置”应用并点击它
- 在“设置”图标
中点击 Google
- 点击登录,然后按照提示登录您的 Google 账号
- 您现在应该已在设备上登录
您的虚拟 Android 设备现已准备就绪,可以进行测试了!
5. 添加依赖项
时长:5:00
为了进行 OAuth API 调用,我们需要先集成所需的库,以便能够发出身份验证请求并使用 Google ID 来发出这些请求:
- libs.googleid
- libs.play.services.auth
- 依次选择“File”>“Project Structure”:
- 然后,依次前往 Dependencies > app > “+” > Library Dependency
- 现在,我们需要添加库:
- 在搜索对话框中,输入 googleid,然后点击搜索
- 应该只有一个条目,请继续选择该条目和可用的最高版本(在本 Codelab 编写时为 1.1.1)
- 点击确定图标
- 重复执行第 1-3 步,但这次搜索 “play-services-auth”,然后选择 Group ID 为“com.google.android.gms”且 Artifact Name 为“play-services-auth”的行
- 点击确定图标
6. 底部动作条流程
底部动作条流程利用 Credential Manager API,让用户能够以简化的方式在 Android 设备上使用 Google 账号登录您的应用。该功能旨在提供快速便捷的体验,尤其是对于回访用户。此流程应在应用启动时触发。
构建登录请求
- 首先,从
MainActivity.kt
中移除Greeting()
和GreetingPreview()
函数,我们不需要它们。 - 现在,我们需要确保为此项目导入所需的软件包。继续操作,在从第 3 行开始的现有
import
语句之后添加以下语句:import android.content.ContentValues.TAG import android.content.Context import android.credentials.GetCredentialException import android.os.Build import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi import androidx.compose.foundation.clickable import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.credentials.CredentialManager import androidx.credentials.exceptions.GetCredentialCancellationException import androidx.credentials.exceptions.GetCredentialCustomException import androidx.credentials.exceptions.NoCredentialException import androidx.credentials.GetCredentialRequest import com.google.android.libraries.identity.googleid.GetGoogleIdOption import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import java.security.SecureRandom import java.util.Base64 import kotlinx.coroutines.CoroutineScope import androidx.compose.runtime.LaunchedEffect import kotlinx.coroutines.delay import kotlinx.coroutines.launch
- 接下来,我们必须创建用于构建底部工作表请求的函数。将此代码粘贴到 MainActivity 类下方
//This line is not needed for the project to build, but you will see errors if it is not present.
//This code will not work on Android versions < UpsideDownCake
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun BottomSheet(webClientId: String) {
val context = LocalContext.current
// LaunchedEffect is used to run a suspend function when the composable is first launched.
LaunchedEffect(Unit) {
// Create a Google ID option with filtering by authorized accounts enabled.
val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
// Create a credential request with the Google ID option.
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()
// Attempt to sign in with the created request using an authorized account
val e = signIn(request, context)
// If the sign-in fails with NoCredentialException, there are no authorized accounts.
// In this case, we attempt to sign in again with filtering disabled.
if (e is NoCredentialException) {
val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOptionFalse)
.build()
//We will build out this function in a moment
signIn(requestFalse, context)
}
}
}
//This function is used to generate a secure nonce to pass in with our request
fun generateSecureRandomNonce(byteLength: Int = 32): String {
val randomBytes = ByteArray(byteLength)
SecureRandom.getInstanceStrong().nextBytes(randomBytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
}
我们来分析一下这段代码的作用:
fun BottomSheet(webClientId: String) {...}
:创建一个名为 BottomSheet 的函数,该函数接受一个名为 webClientid 的字符串实参
val context = LocalContext.current
:检索当前的 Android 上下文。各种操作(包括启动界面组件)都需要此权限。LaunchedEffect(Unit) { ... }
:LaunchedEffect
是一个 Jetpack Compose 可组合项,可让您在可组合项的生命周期内运行挂起函数(一种可以暂停和恢复执行的函数)。将 Unit 用作键意味着,此效应仅在首次启动可组合项时运行一次。val googleIdOption: GetGoogleIdOption = ...
:创建GetGoogleIdOption
对象。此对象用于配置要向 Google 请求的凭据类型。.Builder()
:使用构建器模式来配置选项。.setFilterByAuthorizedAccounts(true)
:指定是否允许用户从所有 Google 账号中进行选择,还是仅允许选择已向应用授予授权的账号。在本例中,该值设置为 true,这意味着如果用户之前曾向此应用授予授权,则系统将使用用户之前向此应用授予授权的凭据来发出请求。.setServerClientId(webClientId)
:设置服务器客户端 ID,这是应用后端的唯一标识符。这是获取 ID 令牌所必需的。.setNonce(generateSecureRandomNonce())
:设置随机值 Nonce,以防范重放攻击并确保 ID 令牌与特定请求相关联。.build()
:使用指定的配置创建GetGoogleIdOption
对象。
val request: GetCredentialRequest = ...
:创建GetCredentialRequest
对象。此对象封装了整个凭据请求。.Builder()
:启动构建器模式以配置请求。.addCredentialOption(googleIdOption)
:向请求添加了 googleIdOption,指定我们想要请求 Google ID 令牌。.build()
:创建GetCredentialRequest
对象。
val e = signIn(request, context)
:此方法会尝试使用创建的请求和当前上下文登录用户。signIn 函数的结果存储在 e 中。此变量将包含成功结果或异常。if (e is NoCredentialException) { ... }
:这是条件检查。如果 signIn 函数因 NoCredentialException 而失败,则表示没有之前已获授权的可用账号。val googleIdOptionFalse: GetGoogleIdOption = ...
:如果之前的signIn
失败,此部分会创建一个新的GetGoogleIdOption
。.setFilterByAuthorizedAccounts(false)
:这是与第一个选项的关键区别。它会停用对已授权账号的过滤,这意味着设备上的任何 Google 账号都可以用于登录。val requestFalse: GetCredentialRequest = ...
:使用googleIdOptionFalse
创建新的GetCredentialRequest
。signIn(requestFalse, context)
:此方法尝试使用允许使用任何账号的新请求登录用户。
从本质上讲,此代码会准备向 Credential Manager API 发出请求,以使用提供的配置检索用户的 Google ID 令牌。然后,可以使用 GetCredentialRequest 启动凭据管理器界面,用户可以在其中选择自己的 Google 账号并授予必要的权限。
fun generateSecureRandomNonce(byteLength: Int = 32): String
:此属性定义了一个名为 generateSecureRandomNonce
的函数。它接受一个整数实参 byteLength(默认值为 32),用于指定随机数的所需长度(以字节为单位)。它会返回一个字符串,该字符串是随机字节的 Base64 编码表示形式。
val randomBytes = ByteArray(byteLength)
:创建一个具有指定 byteLength 的字节数组来保存随机字节。SecureRandom.getInstanceStrong().nextBytes(randomBytes)
:SecureRandom.getInstanceStrong()
:这会获得一个加密强度高的随机数生成器。这对于安全性至关重要,因为它可以确保生成的数字是真正随机的,而不是可预测的。它使用系统中最强的可用熵源。.nextBytes(randomBytes)
:此方法会使用 SecureRandom 实例生成的随机字节填充 randomBytes 数组。
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
:Base64.getUrlEncoder()
:获取使用网址安全字母表(使用 - 和 _ 而不是 + 和 /)的 Base64 编码器。这一点很重要,因为它可以确保生成的字符串可以安全地用于网址,而无需进一步编码。.withoutPadding()
:这会移除 Base64 编码字符串中的所有填充字符。这通常是理想的做法,可使随机数略短一些,更紧凑。.encodeToString(randomBytes)
:此函数会将 randomBytes 编码为 Base64 字符串并返回该字符串。
总而言之,此函数会生成指定长度的强加密随机数,使用可在网址中安全使用的 Base64 对其进行编码,并返回生成的字符串。这是生成可在对安全性要求较高的环境中使用且安全的随机数的标准做法。
发出登录请求
现在,我们已经能够构建登录请求,可以使用 Credential Manager 将其用于登录。为此,我们需要创建一个函数,该函数可处理使用凭据管理器的传递登录请求,同时处理我们可能会遇到的常见异常。
您可以将此函数粘贴到 BottomSheet()
函数下方,以完成此操作。
//This code will not work on Android versions < UPSIDE_DOWN_CAKE when GetCredentialException is
//is thrown.
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
val credentialManager = CredentialManager.create(context)
val failureMessage = "Sign in failed!"
var e: Exception? = null
//using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
//on the initial running of our app
delay(250)
try {
// The getCredential is called to request a credential from Credential Manager.
val result = credentialManager.getCredential(
request = request,
context = context,
)
Log.i(TAG, result.toString())
Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
Log.i(TAG, "(☞゚ヮ゚)☞ Sign in Successful! ☜(゚ヮ゚☜)")
} catch (e: GetCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Failure getting credentials", e)
} catch (e: GoogleIdTokenParsingException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
} catch (e: NoCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": No credentials found", e)
return e
} catch (e: GetCredentialCustomException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
} catch (e: GetCredentialCancellationException) {
Toast.makeText(context, ": Sign-in cancelled", Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
}
return e
}
现在,我们来分析一下此代码的作用:
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception?
:此代码定义了一个名为 signIn 的挂起函数。这意味着它可以暂停和恢复,而不会阻塞主线程。它会返回一个 Exception?
,如果登录成功,该 Exception?
将为 null;如果登录失败,则为特定异常。
它接受两个参数:
request
:一个GetCredentialRequest
对象,其中包含要检索的凭据类型的配置(例如,Google ID)。context
:与系统互动所需的 Android Context。
对于函数正文:
val credentialManager = CredentialManager.create(context)
:创建 CredentialManager 的实例,该实例是与 Credential Manager API 交互的主要接口。应用将通过这种方式启动登录流程。val failureMessage = "Sign in failed!"
:定义在登录失败时要在 Toast 中显示的字符串 (failureMessage)。var e: Exception? = null
:此行初始化一个变量 e,用于存储流程中可能发生的任何异常,初始值为 null。delay(250)
:引入 250 毫秒的延迟。此问题解决方法可解决应用启动时(尤其是在使用 BottomSheet 流程时)可能立即抛出 NoCredentialException 的问题。这样,系统就有时间初始化凭据管理器。try { ... } catch (e: Exception) { ... }
:使用 try-catch 块进行可靠的错误处理。这样可确保在登录过程中发生任何错误时,应用不会崩溃,并且可以妥善处理异常。val result = credentialManager.getCredential(request = request, context = context)
:这是对 Credential Manager API 的实际调用发生的位置,并启动凭据检索流程。它将请求和上下文作为输入,并向用户显示用于选择凭据的界面。如果成功,它将返回包含所选凭据的结果。此运算的结果GetCredentialResponse
存储在result
变量中。Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
:显示一条简短的消息框,指示登录成功。Log.i(TAG, "Sign in Successful!")
:向 logcat 记录一条有趣的成功消息。catch (e: GetCredentialException)
:处理类型为GetCredentialException
的异常。这是在凭据获取过程中可能发生的多个特定异常的父类。catch (e: GoogleIdTokenParsingException)
:处理在解析 Google ID 令牌时发生错误时出现的异常。catch (e: NoCredentialException)
:处理NoCredentialException
,当用户没有可用的凭据时(例如,用户未保存任何凭据,或者没有 Google 账号),系统会抛出此异常。- 至关重要的是,此函数会返回存储在
e
、NoCredentialException
中的异常,从而允许调用方在没有凭据的情况下处理特定情况。
- 至关重要的是,此函数会返回存储在
catch (e: GetCredentialCustomException)
:处理凭据提供方可能抛出的自定义异常。catch (e: GetCredentialCancellationException)
:处理GetCredentialCancellationException
,当用户取消登录流程时会抛出此异常。Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
:显示一条 Toast 消息,指示登录失败,并使用 failureMessage。Log.e(TAG, "", e)
:使用 Log.e 将异常记录到 Android logcat,该方法用于记录错误。这会包含异常的堆栈轨迹,以帮助进行调试。还包含表示愤怒的表情符号,增添趣味。
return e
:如果捕获到任何异常,该函数会返回相应异常;如果登录成功,则返回 null。
总而言之,此代码提供了一种使用 Credential Manager API 处理用户登录的方式,可管理异步操作、处理潜在错误,并通过 Toast 和日志向用户提供反馈,同时在错误处理中添加了一点幽默感。
在应用中实现底部动作条流程
现在,我们可以使用以下代码和之前从 Google Cloud 控制台复制的 Web 应用客户端 ID,设置一个调用来触发 MainActivity
类中的 BottomSheet 流程:
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
//This will trigger on launch
BottomSheet(webClientId)
}
}
}
}
}
现在,我们可以保存项目(文件 > 保存)并运行它:
- 按“运行”按钮:
- 当您的应用在模拟器上运行时,您应该会看到登录 BottomSheet 弹出。点击继续以测试登录
- 您应该会看到一条 Toast 消息,告知您登录成功!
7. 按钮流程
借助“使用 Google 账号登录”的按钮流程,用户可以更轻松地使用其现有的 Google 账号注册或登录您的 Android 应用。如果用户关闭底部工作表,或者只是希望明确使用自己的 Google 账号登录或注册,则会看到此按钮。对于开发者来说,这意味着用户可以更顺畅地完成注册,注册过程中的阻力更小。
虽然可以使用开箱即用的 Jetpack Compose 按钮来实现此目的,但我们将使用“使用 Google 账号登录”品牌推广指南页面中预先批准的品牌图标。
向项目添加品牌图标
- 点击此处下载预先获批的品牌图标的 ZIP 文件
- 从下载内容中解压缩 signin-assest.zip(这会因计算机的操作系统而异)。现在,您可以打开 signin-assets 文件夹,然后浏览可用的图标。在此 Codelab 中,我们将使用
signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png
。 - 复制文件
- 在 Android Studio 中,右键点击 drawable 文件夹,然后点击 Paste,将图片粘贴到 res > drawable 下的项目中(您可能需要展开 res 文件夹才能看到它)
- 系统会显示一个对话框,提示您重命名文件并确认要将该文件添加到哪个目录。将素材资源重命名为 siwg_button.png,然后点击确定
按钮流代码
此代码将使用与 BottomSheet()
相同的 signIn()
函数,但使用 GetSignInWithGoogleOption
而不是 GetGoogleIdOption
,因为此流程不会利用设备上存储的凭据和通行密钥来显示登录选项。以下代码可粘贴到 BottomSheet()
函数下方:
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val onClick: () -> Unit = {
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
.Builder(serverClientId = webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()
coroutineScope.launch {
signIn(request, context)
}
}
Image(
painter = painterResource(id = R.drawable.siwg_button),
contentDescription = "",
modifier = Modifier
.fillMaxSize()
.clickable(enabled = true, onClick = onClick)
)
}
以下是代码的细分说明:
fun ButtonUI(webClientId: String)
:此代码声明了一个名为 ButtonUI
的函数,该函数接受 webClientId
(您的 Google Cloud 项目的客户端 ID)作为实参。
val context = LocalContext.current
:检索当前的 Android 上下文。各种操作(包括启动界面组件)都需要此权限。
val coroutineScope = rememberCoroutineScope()
:创建协程范围。它用于管理异步任务,使代码能够在不阻塞主线程的情况下运行。rememberCoroutineScope
() 是 Jetpack Compose 中的一个可组合函数,它将提供一个与可组合函数的生命周期绑定的作用域。
val onClick: () -> Unit = { ... }
:这会创建一个 lambda 函数,该函数将在点击按钮时执行。相应 lambda 函数将:
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder(serverClientId = webClientId).setNonce(generateSecureRandomNonce()).build()
:此部分用于创建GetSignInWithGoogleOption
对象。此对象用于指定“使用 Google 账号登录”流程的参数,需要webClientId
和随机数(用于安全性的随机字符串)。val request: GetCredentialRequest = GetCredentialRequest.Builder().addCredentialOption(signInWithGoogleOption).build()
:用于构建GetCredentialRequest
对象。此请求将用于通过 Credential Manager 获取用户凭据。GetCredentialRequest
会将之前创建的GetSignInWithGoogleOption
添加为选项,以便请求“使用 Google 账号登录”凭据。
coroutineScope.launch { ... }
:用于管理异步操作(使用协程)的CoroutineScope
。signIn(request, context)
:调用我们之前定义的signIn
() 函数
Image(...)
:这会使用加载图片 R.drawable.siwg_button
的 painterResource
渲染图片
Modifier.fillMaxSize().clickable(enabled = true, onClick = onClick)
:fillMaxSize()
:使图片填充可用空间。clickable(enabled = true, onClick = onClick)
:使图片可点击,并在点击时执行之前定义的 onClick lambda 函数。
总而言之,此代码会在 Jetpack Compose 界面中设置“使用 Google 账号登录”按钮。当用户点击该按钮时,系统会准备一个凭据请求,以启动凭据管理器并允许用户使用其 Google 账号登录。
现在,我们需要更新 MainActivity 类以运行 ButtonUI()
函数:
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
//This will trigger on launch
BottomSheet(webClientId)
//This requires the user to press the button
ButtonUI(webClientId)
}
}
}
}
}
}
现在,我们可以保存项目(文件 > 保存)并运行它:
- 按“运行”按钮:
- 应用在模拟器上运行后,系统应显示 BottomSheet。点击该窗口以外的区域即可将其关闭。
- 现在,您应该会在应用中看到我们创建的按钮。点击该按钮,即可看到登录对话框
- 点击您的账号即可登录!
8. 总结
您已完成此 Codelab!如需详细了解或获取有关 Android 设备上“使用 Google 账号登录”功能的帮助,请参阅下方的“常见问题解答”部分:
常见问题解答
完整的 MainActivity.kt 代码
以下是 MainActivity.kt 的完整代码,供您参考:
package com.example.example
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.example.example.ui.theme.ExampleTheme
import android.content.ContentValues.TAG
import android.content.Context
import android.credentials.GetCredentialException
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.credentials.CredentialManager
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialCustomException
import androidx.credentials.exceptions.NoCredentialException
import androidx.credentials.GetCredentialRequest
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import java.security.SecureRandom
import java.util.Base64
import kotlinx.coroutines.CoroutineScope
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//replace with your own web client ID from Google Cloud Console
val webClientId = "YOUR_CLIENT_ID_HERE"
setContent {
//ExampleTheme - this is derived from the name of the project not any added library
//e.g. if this project was named "Testing" it would be generated as TestingTheme
ExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
//This will trigger on launch
BottomSheet(webClientId)
//This requires the user to press the button
ButtonUI(webClientId)
}
}
}
}
}
}
//This line is not needed for the project to build, but you will see errors if it is not present.
//This code will not work on Android versions < UpsideDownCake
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun BottomSheet(webClientId: String) {
val context = LocalContext.current
// LaunchedEffect is used to run a suspend function when the composable is first launched.
LaunchedEffect(Unit) {
// Create a Google ID option with filtering by authorized accounts enabled.
val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
// Create a credential request with the Google ID option.
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()
// Attempt to sign in with the created request using an authorized account
val e = signIn(request, context)
// If the sign-in fails with NoCredentialException, there are no authorized accounts.
// In this case, we attempt to sign in again with filtering disabled.
if (e is NoCredentialException) {
val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOptionFalse)
.build()
signIn(requestFalse, context)
}
}
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val onClick: () -> Unit = {
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
.Builder(serverClientId = webClientId)
.setNonce(generateSecureRandomNonce())
.build()
val request: GetCredentialRequest = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()
signIn(coroutineScope, request, context)
}
Image(
painter = painterResource(id = R.drawable.siwg_button),
contentDescription = "",
modifier = Modifier
.fillMaxSize()
.clickable(enabled = true, onClick = onClick)
)
}
fun generateSecureRandomNonce(byteLength: Int = 32): String {
val randomBytes = ByteArray(byteLength)
SecureRandom.getInstanceStrong().nextBytes(randomBytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
}
//This code will not work on Android versions < UPSIDE_DOWN_CAKE when GetCredentialException is
//is thrown.
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
val credentialManager = CredentialManager.create(context)
val failureMessage = "Sign in failed!"
var e: Exception? = null
//using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
//on the initial running of our app
delay(250)
try {
// The getCredential is called to request a credential from Credential Manager.
val result = credentialManager.getCredential(
request = request,
context = context,
)
Log.i(TAG, result.toString())
Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
Log.i(TAG, "(☞゚ヮ゚)☞ Sign in Successful! ☜(゚ヮ゚☜)")
} catch (e: GetCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Failure getting credentials", e)
} catch (e: GoogleIdTokenParsingException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
} catch (e: NoCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": No credentials found", e)
return e
} catch (e: GetCredentialCustomException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
} catch (e: GetCredentialCancellationException) {
Toast.makeText(context, ": Sign-in cancelled", Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
}
return e
}