1. Прежде чем начать
Узнайте, как реализовать вход через Google на Android с помощью Credential Manager.
Предварительные требования
- Базовое понимание Kotlin для разработки под Android.
- Базовое понимание Jetpack Compose ( подробнее ).
Что вы узнаете
- Создайте проект Google Cloud и клиенты OAuth.
- Реализуйте процедуру входа через нижний интерфейс.
- Реализуйте явный процесс авторизации с помощью кнопки.
Что вам нужно
- Установлена программа Android Studio .
- Компьютер, соответствующий системным требованиям Android Studio и эмулятора.
- Установлен комплект разработки Java (JDK) .
2. Создайте проект в Android Studio.
Для начала создайте новый проект в Android Studio:
- Откройте Android Studio и нажмите «Новый проект» .

- Выберите «Телефон и планшет» > «Очистить активность» , затем нажмите «Далее» .

- Настройте параметры проекта:
- Название : Выберите название проекта.
- Название пакета : Используйте название по умолчанию или выберите своё.
- Минимальный уровень SDK : выберите самую последнюю стабильную версию или Android 14 или выше .

- Нажмите «Готово» и дождитесь завершения первоначальной сборки проекта.

3. Настройте свой проект в Google Cloud.
Создайте проект в Google Cloud.
- Перейдите в консоль Google Cloud и выберите или создайте проект.

- Перейдите в раздел API и сервисы > Экран согласия OAuth .

- Нажмите «Начать» и заполните необходимые поля:
- Название приложения : Используйте название вашего приложения для Android.
- Адрес электронной почты службы поддержки пользователей : выберите свой аккаунт Google.
- Целевая аудитория : выберите «Внешняя» .
- Контактная информация : Введите свой адрес электронной почты.

- Ознакомьтесь с политикой Google API Services: User Data Policy и нажмите «Создать» .

Настройка клиентов OAuth
Для получения идентификаторов клиентов для аутентификации необходимо создать как веб-клиент , так и Android-клиент в консоли Google Cloud.
- Клиент для Android : обеспечивает безопасность запросов путем проверки имени пакета вашего приложения и подписи SHA-1.
- Веб-клиент : выступает в качестве серверной части для сервиса авторизации Google.
Создание Android OAuth 2.0 клиента
- На странице «Клиенты» нажмите «Создать клиент» и выберите Android в качестве типа приложения .

- Введите имя пакета вашего приложения (соответствующее строке 1 файла
MainActivity.kt). - Сгенерируйте свою подпись SHA-1. Откройте терминал Android Studio и выполните команду: macOS/Linux:
Окна:keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
keytool -list -v -keystore "C:\Users\USERNAME\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android
- Скопируйте отпечаток SHA-1 из вывода команды, вставьте его в поле «Отпечаток SHA-1» в консоли и нажмите «Создать» .

Создание клиента Web OAuth 2.0
- Снова нажмите «Создать клиента» и выберите «Веб-приложение» в качестве типа приложения .
- Назовите свой веб-клиент, оставьте поля URL/Origins пустыми и нажмите «Создать» .

- Скопируйте сгенерированный идентификатор клиента из диалогового окна подтверждения. Вы будете использовать его в своем коде Kotlin.

4. Настройка виртуального устройства Android
Для тестирования приложения можно использовать физическое устройство Android или виртуальное устройство Android (AVD).
Создайте и запустите AVD.
- В Android Studio откройте Диспетчер устройств , нажмите «Создать виртуальное устройство» (или значок «+» ) и выберите «Средний телефон» .
- Выберите самую последнюю стабильную версию в качестве образа системы и нажмите «Готово» .
- Запустите эмулятор, нажав на значок «Играть/Запустить» рядом с устройством.

Войдите в учетную запись Google на устройстве.
- В эмуляторе откройте приложение «Настройки» и перейдите в раздел Google .
- Нажмите «Войти в свой аккаунт Google» и следуйте инструкциям.

5. Добавьте зависимости
Добавьте в свой проект библиотеки, необходимые для аутентификации и интеграции с Google ID:
- Перейдите в меню Файл > Структура проекта > Зависимости > app .
- Нажмите + > Зависимости библиотеки , найдите
com.google.android.libraries.identity.googleid:googleidи выберите последнюю версию (например,1.1.1). - Снова нажмите + > Зависимость библиотеки , найдите
play-services-authи выберите библиотеку с идентификатором группыcom.google.android.gms. - Нажмите ОК , чтобы применить изменения и синхронизировать проект.

6. Реализуйте алгоритм построения нижней панели.

Функция «Нижний экран» использует API диспетчера учетных данных , чтобы обеспечить пользователям удобный способ входа в ваше приложение с помощью своих учетных записей Google на Android . Разработанная для скорости и удобства, особенно для постоянных пользователей, эта функция должна запускаться при запуске приложения.
Сформировать запрос на вход в систему.
- Для начала откройте
MainActivity.ktи удалите функцииGreeting()иGreetingPreview()по умолчанию. - Добавьте следующие операторы импорта после существующих, начиная со строки 3:
import android.content.Context import android.os.Build import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable 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.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.credentials.CredentialManager import androidx.credentials.CustomCredential import androidx.credentials.GetCredentialRequest import androidx.credentials.exceptions.GetCredentialCancellationException import androidx.credentials.exceptions.GetCredentialCustomException import androidx.credentials.exceptions.GetCredentialException import androidx.credentials.exceptions.NoCredentialException import com.google.android.libraries.identity.googleid.GetGoogleIdOption import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import java.security.SecureRandom import java.util.Base64 import kotlinx.coroutines.delay import kotlinx.coroutines.launch const val TAG = "MainActivity" - Добавьте эту составную функцию ниже класса
MainActivityв файлеMainActivity.kt:@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) }
Разбор кода
-
LaunchedEffect(Unit): Запускает процесс авторизации немедленно при первом отображении объекта Composable. -
GetGoogleIdOption.Builder(): Настраивает запрос токена Google ID.-
setFilterByAuthorizedAccounts(true): Сначала выполняется попытка бесшумного входа в систему путем фильтрации учетных записей, которые пользователь уже авторизовал для этого приложения, что минимизирует неудобства для повторно использующих приложение пользователей. -
setNonce(...): Передает безопасный случайный nonce, генерируемый для каждого запроса функциейgenerateSecureRandomNonce()для предотвращения атак повторного воспроизведения.
-
-
signIn(request, context): Выполняет запрос. Если он завершается ошибкойNoCredentialException(что означает отсутствие ранее авторизованной учетной записи), процесс переключается наsetFilterByAuthorizedAccounts(false), позволяя пользователю выбрать любую учетную запись Google, авторизованную на устройстве.
Отправьте запрос на вход в систему.
После создания запроса на вход в систему вы можете использовать диспетчер учетных данных для завершения процесса авторизации. Создайте функцию с именем signIn , которая выполняет запрос и обрабатывает распространенные исключения, которые могут возникнуть.
Добавьте эту функцию ниже функции BottomSheet в файле MainActivity.kt :
@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!"
//using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
//on the initial running of our app
delay(250)
return 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())
val credential = result.credential
if (credential is CustomCredential &&
credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
Log.i(TAG, "Signed in as: ${googleIdTokenCredential.id}")
}
Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
Log.i(TAG, "(☞゚ヮ゚)☞ Sign in Successful! ☜(゚ヮ゚☜)")
null
} catch (e: GoogleIdTokenParsingException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
e
} catch (e: NoCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": No credentials found", e)
e
} catch (e: GetCredentialCancellationException) {
Toast.makeText(context, "Sign-in cancelled", Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
e
} catch (e: GetCredentialCustomException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
e
} catch (e: GetCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Failure getting credentials", e)
e
}
}
Разбор кода
-
credentialManager.getCredential(...): Вызывает API диспетчера учетных данных для отображения нижней панели или диалогового окна выбора системной учетной записи. -
delay(250): Кратковременная пауза для предотвращения состояния гонки, когда нижний экран запускается сразу после запуска приложения, до того, как служба диспетчера учетных данных завершит инициализацию. - Обработка исключений : перехватывает и регистрирует распространенные ошибки учетных данных (например, отмена, отсутствие учетных данных или проблемы с анализом токена) и предоставляет пользователю обратную связь в виде всплывающих уведомлений.
Запустить поток нижнего слоя
Обновите класс MainActivity , чтобы он вызывал BottomSheet() при запуске. Замените YOUR_CLIENT_ID_HERE на идентификатор клиента вашего веб-приложения:
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)
}
}
}
}
}
Сохраните свой проект ( Файл > Сохранить ) и запустите приложение:
- Нажмите кнопку запуска:

- При запуске приложения на эмуляторе должно появиться всплывающее окно входа в систему. Нажмите «Продолжить» , чтобы проверить процесс.

- Должно появиться всплывающее уведомление, подтверждающее успешный вход в систему.

7. Реализуйте последовательность действий с помощью кнопок.

Кнопка предоставляет пользователям явную возможность войти в систему или зарегистрироваться. Использование стандартного фирменного стиля обеспечивает единообразие пользовательского опыта. Используйте предварительно одобренные элементы, соответствующие рекомендациям по фирменному стилю «Вход через Google» .
Добавьте значок бренда
- Скачайте фирменные элементы здесь и распакуйте ZIP-архив.
- Copy
signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png. - В Android Studio скопируйте файл в папку res > drawable , переименуйте его в
siwg_button.pngи нажмите OK .
Код обработки нажатия кнопки
В этом потоке используется та же вспомогательная функция signIn , но вместо GetGoogleIdOption передается GetSignInWithGoogleOption . В отличие от потока Bottom Sheet, поток с явной кнопкой не выполняет предварительную фильтрацию и не запрашивает автоматически сохраненные учетные данные или пароли. Вставьте эту составную функцию ниже функции 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)
)
}
Разбор кода
-
GetSignInWithGoogleOption: В отличие от сценария с нижней панелью инструментов, сценарий с явной кнопкой использует эту опцию, чтобы предложить пользователям выбрать свою учетную запись Google без автоматической фильтрации. -
coroutineScope.launch: Запускает сопрограмму для асинхронного выполнения функции suspendsignInпри нажатии кнопки. -
Image: Отображает фирменный рисунокsiwg_buttonи добавляет обработчик клика для запуска процесса.
Добавьте кнопку в макет пользовательского интерфейса.
Обновите макет MainActivity , чтобы отображать как автоматически созданный BottomSheet , так и явно созданный 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)
}
}
}
}
}
}
Проверьте работу кнопок.
- Запустите приложение.
- Чтобы закрыть нижний лист, щелкните за пределами его области.
- Нажмите кнопку «Войти через Google» , чтобы открыть диалоговое окно входа, и выберите свою учетную запись.

- Проверьте результат: проверьте Logcat, чтобы убедиться в наличии вашего имени пользователя/адреса электронной почты.
8. Заключение
Поздравляем! Вы успешно реализовали вход через Google с помощью Android Credential Manager.
Дополнительные ресурсы
- Документация по менеджеру учетных данных Android
- Руководство по устранению неполадок диспетчера учетных данных
- Часто задаваемые вопросы о менеджере учетных данных
- Поддержка StackOverflow
Полный код файла 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.util.Log
import android.widget.Toast
import androidx.credentials.exceptions.GetCredentialException
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)
}
}
}
}
}
}
@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()
coroutineScope.launch {
signIn(request, context)
}
}
Image(
painter = painterResource(id = R.drawable.siwg_button),
contentDescription = "",
modifier = Modifier
.fillMaxSize()
.clickable(onClick = onClick)
)
}
fun generateSecureRandomNonce(byteLength: Int = 32): String {
val randomBytes = ByteArray(byteLength)
SecureRandom.getInstanceStrong().nextBytes(randomBytes)
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
}
@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!"
//using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
//on the initial running of our app
delay(250)
return 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())
val credential = result.credential
if (credential is CustomCredential &&
credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
Log.i(TAG, "Signed in as: ${googleIdTokenCredential.id}")
}
Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
Log.i(TAG, "(☞゚ヮ゚)☞ Sign in Successful! ☜(゚ヮ゚☜)")
null
} catch (e: GoogleIdTokenParsingException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
e
} catch (e: NoCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": No credentials found", e)
e
} catch (e: GetCredentialCancellationException) {
Toast.makeText(context, "Sign-in cancelled", Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
e
} catch (e: GetCredentialCustomException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
e
} catch (e: GetCredentialException) {
Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
Log.e(TAG, failureMessage + ": Failure getting credentials", e)
e
}
}