1. Zanim zaczniesz
Z tego laboratorium dowiesz się, jak zaimplementować na Androidzie funkcję Zaloguj się przez Google za pomocą Menedżera danych logowania.
Wymagania wstępne
- Podstawowa wiedza na temat korzystania z Kotlin do tworzenia aplikacji na Androida.
- Podstawowa znajomość Jetpack Compose (więcej informacji znajdziesz tutaj).
Czego się nauczysz
- Jak utworzyć projekt Google Cloud
- Jak tworzyć klienty OAuth w konsoli Google Cloud
- Jak wdrożyć funkcję Zaloguj się przez Google za pomocą przepływu arkusza u dołu ekranu
- Jak wdrożyć funkcję Zaloguj się przez Google za pomocą procedury przycisku
Wymagania
- Android Studio (pobierz tutaj)
- Komputer spełniający wymagania systemowe Androida Studio.
- Komputer spełniający wymagania systemowe emulatora Androida
2. Tworzenie projektu Android Studio
Czas trwania: 3:00–5:00
Na początek musimy utworzyć nowy projekt w Android Studio:
- Otwórz Android Studio
- Kliknij Nowy projekt
.
- Wybierz Telefon i tablet oraz Puste działanie
- Kliknij Dalej.
- Teraz czas skonfigurować kilka elementów projektu:
- Nazwa: nazwa projektu.
- Nazwa pakietu: zostanie wypełniona automatycznie na podstawie nazwy projektu.
- Lokalizacja zapisu: domyślnie powinien to być folder, w którym Android Studio zapisuje projekty. Możesz to zmienić w dowolnym momencie.
- Minimalny pakiet SDK: jest to najstarsza wersja pakietu Android SDK, na której Twoja aplikacja może działać. W tym module będziemy używać interfejsu API 36 (Baklava).
- Kliknij Zakończ.
- Android Studio utworzy projekt i pobierze wszystkie niezbędne zależności dla podstawowej aplikacji. Może to potrwać kilka minut. Aby to zobaczyć, kliknij ikonę kompilacji:
- Po zakończeniu tego procesu środowisko Android Studio powinno wyglądać podobnie do tego:
3. Konfigurowanie projektu Google Cloud
Tworzenie projektu Google Cloud
- Otwórz konsolę Google Cloud.
- Otwórz projekt lub utwórz nowy
- Kliknij Interfejsy API i usługi
- Otwórz ekran zgody OAuth
.
- Aby kontynuować, musisz wypełnić pola w sekcji Przegląd. Kliknij Rozpocznij, aby zacząć wypełniać te informacje:
- Nazwa aplikacji: nazwa aplikacji, która powinna być taka sama jak nazwa użyta podczas tworzenia projektu w Android Studio.
- Adres e-mail pomocy dla użytkowników: wyświetli konto Google, na które się logujesz, oraz wszystkie Grupy dyskusyjne Google, którymi zarządzasz.
- Odbiorcy:
- Wewnętrzna – w przypadku aplikacji używanej tylko w organizacji. Jeśli z projektem Google Cloud nie jest powiązana żadna organizacja, nie możesz go wybrać.
- Będziemy korzystać z opcji Zewnętrzny.
- Dane kontaktowe: może to być dowolny adres e-mail, który ma być punktem kontaktowym dla aplikacji.
- Zapoznaj się z zasadami dotyczącymi danych użytkownika w usługach interfejsów API Google.
- Po przeczytaniu i zaakceptowaniu zasad dotyczących danych użytkownika kliknij Utwórz
Konfigurowanie klientów OAuth
Po skonfigurowaniu projektu Google Cloud musimy dodać klienta internetowego i klienta Androida, aby móc wywoływać interfejsy API na serwerze backendu OAuth za pomocą ich identyfikatorów klienta.
W przypadku klienta internetowego na Androida musisz mieć:
- Nazwa pakietu aplikacji (np. com.example.example)
- Podpis SHA-1 aplikacji
- Co to jest podpis SHA-1?
- Odcisk cyfrowy SHA-1 to skrót kryptograficzny wygenerowany na podstawie klucza podpisywania aplikacji. Jest to unikalny identyfikator certyfikatu podpisywania konkretnej aplikacji. Można go porównać do cyfrowego „podpisu” aplikacji.
- Dlaczego potrzebujemy podpisu SHA-1?
- Odcisk cyfrowy SHA-1 zapewnia, że tylko Twoja aplikacja podpisana określonym kluczem podpisywania może prosić o tokeny dostępu przy użyciu identyfikatora klienta OAuth 2.0. Zapobiega to uzyskiwaniu dostępu do zasobów projektu i danych użytkowników przez inne aplikacje (nawet te o tej samej nazwie pakietu).
- Wyobraź sobie, że:
- Klucz podpisywania aplikacji jest jak fizyczny klucz do „drzwi” aplikacji. Umożliwia on dostęp do wewnętrznych mechanizmów aplikacji.
- Odcisk cyfrowy SHA-1 jest jak unikalny identyfikator karty dostępu, który jest powiązany z fizycznym kluczem. Jest to konkretny kod, który identyfikuje dany klucz.
- Identyfikator klienta OAuth 2.0 jest jak kod dostępu do konkretnego zasobu lub usługi Google (np. Logowanie przez Google).
- Podając odcisk SHA-1 podczas konfigurowania klienta OAuth, mówisz Google: „Tylko karta z tym konkretnym identyfikatorem (SHA-1) może otworzyć ten kod dostępu (identyfikator klienta)”. Dzięki temu tylko Twoja aplikacja będzie mieć dostęp do usług Google połączonych z tym kodem."
- Co to jest podpis SHA-1?
W przypadku klienta internetowego wystarczy podać nazwę, której chcesz używać do identyfikacji klienta w konsoli.
Tworzenie klienta OAuth 2.0 na Androida
- Otwórz stronę Klienci.
- Kliknij Utwórz klienta
- Jako typ aplikacji wybierz Android.
- Musisz podać nazwę pakietu aplikacji
- W Androidzie Studio musimy uzyskać podpis SHA-1 naszej aplikacji i skopiować go tutaj:
- Otwórz Android Studio i terminal.
- Uruchom to polecenie:
To polecenie służy do wyświetlania szczegółów konkretnego wpisu (aliasu) w magazynie kluczy.keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
-list
: Ta opcja nakazuje narzędziu keytool wyświetlenie zawartości magazynu kluczy.-v
: ta opcja włącza szczegółowe dane wyjściowe, które zawierają więcej informacji o wpisie.-keystore ~/.android/debug.keystore
: Określa ścieżkę do pliku magazynu kluczy.-alias androiddebugkey
: określa alias (nazwę wpisu) klucza, który chcesz sprawdzić.-storepass android
: podaje hasło do pliku magazynu kluczy.-keypass android
: podaje hasło do klucza prywatnego określonego aliasu.
- Skopiuj wartość podpisu SHA-1:
- Wróć do okna Google Cloud i wklej wartość podpisu SHA-1:
- Ekran powinien teraz wyglądać podobnie do tego, a Ty możesz kliknąć Utwórz:
Tworzenie klienta internetowego OAuth 2.0
- Aby utworzyć identyfikator klienta aplikacji internetowej, powtórz kroki 1–2 z sekcji Tworzenie klienta Androida i jako typ aplikacji wybierz Aplikacja internetowa.
- Nadaj klientowi nazwę (będzie to klient OAuth):
- Kliknij Utwórz.
- Skopiuj identyfikator klienta z wyskakującego okienka. Będzie on potrzebny później.
Po skonfigurowaniu klientów OAuth możemy wrócić do Android Studio i utworzyć aplikację na Androida do logowania się za pomocą Google.
4. Konfigurowanie urządzenia wirtualnego z Androidem
Aby szybko przetestować aplikację bez fizycznego urządzenia z Androidem, utwórz urządzenie wirtualne z Androidem, na którym możesz skompilować i natychmiast uruchomić aplikację z Android Studio. Jeśli chcesz przeprowadzić test na fizycznym urządzeniu z Androidem, postępuj zgodnie z instrukcjami w dokumentacji dla deweloperów aplikacji na Androida.
Tworzenie wirtualnego urządzenia z Androidem
- W Android Studio otwórz Menedżera urządzeń
.
- Kliknij przycisk + > Utwórz urządzenie wirtualne
- W tym miejscu możesz dodać dowolne urządzenie potrzebne do projektu. Na potrzeby tego Codelabu wybierz Medium Phone (Średni telefon) i kliknij Dalej
.
- Teraz możesz skonfigurować urządzenie dla swojego projektu, nadając mu unikalną nazwę, wybierając wersję Androida, na której będzie działać, i wykonując inne czynności. Sprawdź, czy interfejs API jest ustawiony na API 36 „Baklava”; Android 16, a potem kliknij Zakończ
.
- Nowe urządzenie powinno pojawić się w Menedżerze urządzeń. Aby sprawdzić, czy urządzenie działa, kliknij
obok utworzonego urządzenia
.
- Urządzenie powinno się teraz włączyć.
Logowanie się na wirtualnym urządzeniu z Androidem
Utworzone urządzenie działa. Aby uniknąć błędów podczas testowania logowania się przez Google, musimy zalogować się na urządzeniu za pomocą konta Google.
- Otwórz Ustawienia:
- Kliknij środek ekranu urządzenia wirtualnego i przesuń palcem w górę.
- Znajdź aplikację Ustawienia i kliknij ją.
- W Ustawieniach kliknij Google
- Kliknij Zaloguj się i postępuj zgodnie z instrukcjami, aby zalogować się na konto Google
.
- Powinno nastąpić zalogowanie na urządzeniu.
Wirtualne urządzenie z Androidem jest gotowe do testowania.
5. Dodawanie zależności
Czas trwania: 5:00
Aby wywoływać interfejs API OAuth, musimy najpierw zintegrować potrzebne biblioteki, które umożliwiają wysyłanie żądań uwierzytelniania i używanie identyfikatorów Google do wysyłania tych żądań:
- libs.googleid
- libs.play.services.auth
- Kliknij kolejno File > Project Structure (Plik > Struktura projektu):
- Następnie otwórz Dependencies (Zależności) > app (aplikacja) > „+” > Library Dependency (Zależność biblioteki)
.
- Teraz musimy dodać nasze biblioteki:
- W oknie wyszukiwania wpisz googleid i kliknij Szukaj.
- Powinien być tylko jeden wpis. Wybierz go i najnowszą dostępną wersję (w momencie tworzenia tego kursu jest to wersja 1.1.1).
- Kliknij OK
- Powtórz kroki 1–3, ale zamiast tego wyszukaj „play-services-auth” i wybierz wiersz z wartością „com.google.android.gms” w kolumnie Identyfikator grupy oraz „play-services-auth” w kolumnie Nazwa artefaktu.
- Kliknij OK
6. Przepływ planszy dolnej
Przepływ arkusza u dołu ekranu korzysta z interfejsu Credential Manager API, aby umożliwić użytkownikom logowanie się w aplikacji na Androidzie za pomocą kont Google. Został on zaprojektowany tak, aby był szybki i wygodny, zwłaszcza dla powracających użytkowników. Ten proces powinien być uruchamiany przy uruchomieniu aplikacji.
Tworzenie żądania logowania
- Na początek usuń funkcje
Greeting()
iGreetingPreview()
z funkcjiMainActivity.kt
– nie będą nam potrzebne. - Teraz musimy się upewnić, że pakiety, których potrzebujemy, są zaimportowane do tego projektu. Dodaj te instrukcje
import
po dotychczasowych, zaczynając od wiersza 3: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
- Następnie musimy utworzyć funkcję do tworzenia żądania Bottom Sheet. Wklej ten kod pod klasą 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)
}
Przyjrzyjmy się, co robi ten kod:
fun BottomSheet(webClientId: String) {...}
: tworzy funkcję o nazwie BottomSheet, która przyjmuje 1 argument tekstowy o nazwie webClientid.
val context = LocalContext.current
: pobiera bieżący kontekst Androida. Jest to wymagane w przypadku różnych operacji, w tym uruchamiania komponentów interfejsu.LaunchedEffect(Unit) { ... }
:LaunchedEffect
to funkcja kompozycyjna Jetpack Compose, która umożliwia uruchamianie funkcji zawieszającej (funkcji, która może wstrzymywać i wznawiać wykonanie) w cyklu życia funkcji kompozycyjnej. Jednostka jako klucz oznacza, że ten efekt zostanie uruchomiony tylko raz, gdy funkcja kompozycyjna zostanie uruchomiona po raz pierwszy.val googleIdOption: GetGoogleIdOption = ...
: tworzy obiektGetGoogleIdOption
. Ten obiekt konfiguruje typ danych logowania, o które użytkownik prosi Google..Builder()
: do konfigurowania opcji używany jest wzorzec konstruktora..setFilterByAuthorizedAccounts(true)
: określa, czy użytkownik może wybierać spośród wszystkich kont Google, czy tylko tych, które już autoryzowały aplikację. W tym przypadku wartość to „true”, co oznacza, że żądanie zostanie wysłane przy użyciu danych logowania, które użytkownik wcześniej autoryzował do użycia w tej aplikacji (jeśli takie dane są dostępne)..setServerClientId(webClientId)
: ustawia identyfikator klienta serwera, który jest unikalnym identyfikatorem backendu aplikacji. Jest to wymagane, aby uzyskać token identyfikatora..setNonce(generateSecureRandomNonce())
: ustawia wartość jednorazową, czyli losową wartość, aby zapobiec atakom typu replay i zapewnić, że token identyfikatora jest powiązany z określonym żądaniem..build()
: tworzy obiektGetGoogleIdOption
z określoną konfiguracją.
val request: GetCredentialRequest = ...
: tworzy obiektGetCredentialRequest
. Ten obiekt zawiera całe żądanie danych logowania..Builder()
: uruchamia wzorzec konstruktora, aby skonfigurować żądanie..addCredentialOption(googleIdOption)
: dodaje do żądania parametr googleIdOption, który określa, że chcemy poprosić o token identyfikatora Google..build()
: tworzy obiektGetCredentialRequest
.
val e = signIn(request, context)
: próbuje zalogować użytkownika za pomocą utworzonego żądania i bieżącego kontekstu. Wynik funkcji signIn jest przechowywany w e. Ta zmienna będzie zawierać wynik lub wyjątek.if (e is NoCredentialException) { ... }
: jest to sprawdzanie warunkowe. Jeśli funkcja signIn zakończy się niepowodzeniem z wyjątkiem NoCredentialException, oznacza to, że nie ma dostępnych wcześniej autoryzowanych kont.val googleIdOptionFalse: GetGoogleIdOption = ...
: jeśli poprzedni waruneksignIn
nie został spełniony, ta część tworzy nowy warunekGetGoogleIdOption
..setFilterByAuthorizedAccounts(false)
: to jest kluczowa różnica w porównaniu z pierwszą opcją. Wyłącza filtrowanie autoryzowanych kont, co oznacza, że do logowania się można używać dowolnego konta Google na urządzeniu.val requestFalse: GetCredentialRequest = ...
: Tworzony jest nowyGetCredentialRequest
z wartościągoogleIdOptionFalse
.signIn(requestFalse, context)
: próba zalogowania użytkownika za pomocą nowego żądania, które umożliwia użycie dowolnego konta.
Ten kod przygotowuje żądanie do interfejsu Credential Manager API, aby pobrać token identyfikatora Google dla użytkownika przy użyciu podanych konfiguracji. Żądanie GetCredentialRequest można następnie wykorzystać do uruchomienia interfejsu menedżera danych logowania, w którym użytkownik może wybrać swoje konto Google i przyznać niezbędne uprawnienia.
fun generateSecureRandomNonce(byteLength: Int = 32): String
: Definiuje to funkcję o nazwie generateSecureRandomNonce
. Przyjmuje argument całkowity byteLength (z wartością domyślną 32), który określa żądaną długość nonce w bajtach. Zwraca ciąg tekstowy, który jest reprezentacją losowych bajtów zakodowaną w standardzie Base64.
val randomBytes = ByteArray(byteLength)
: tworzy tablicę bajtów o określonej długości, która będzie zawierać losowe bajty.SecureRandom.getInstanceStrong().nextBytes(randomBytes)
:SecureRandom.getInstanceStrong()
: zapewnia to generator liczb losowych o właściwościach kryptograficznych. Jest to kluczowe dla bezpieczeństwa, ponieważ zapewnia, że wygenerowane liczby są rzeczywiście losowe i nieprzewidywalne. Wykorzystuje najsilniejsze dostępne źródło entropii w systemie..nextBytes(randomBytes)
: wypełnia tablicę randomBytes losowymi bajtami wygenerowanymi przez instancję SecureRandom.
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
:Base64.getUrlEncoder()
: pobiera koder Base64, który używa alfabetu bezpiecznego dla adresów URL (zamiast znaków + i / używa znaków - i _). Jest to ważne, ponieważ dzięki temu wynikowy ciąg znaków może być bezpiecznie używany w adresach URL bez konieczności dalszego kodowania..withoutPadding()
: usuwa znaki dopełnienia z ciągu zakodowanego w Base64. Często jest to pożądane, aby skrócić i zmniejszyć rozmiar nonce..encodeToString(randomBytes)
: koduje losowe bajty w ciąg Base64 i zwraca go.
Podsumowując, ta funkcja generuje kryptograficznie silny losowy nonce o określonej długości, koduje go za pomocą bezpiecznego dla adresu URL kodowania Base64 i zwraca wynikowy ciąg znaków. Jest to standardowa praktyka generowania wartości nonce, które są bezpieczne w kontekstach wymagających szczególnej ochrony.
Wysyłanie prośby o zalogowanie
Teraz, gdy możemy utworzyć żądanie logowania, możemy użyć Menedżera danych logowania, aby się zalogować. Aby to zrobić, musimy utworzyć funkcję, która będzie obsługiwać przekazywanie żądań logowania za pomocą Menedżera danych logowania, a także typowe wyjątki, które mogą wystąpić.
Aby to zrobić, możesz wkleić tę funkcję pod funkcją 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
}
Teraz omówmy, co robi ten kod:
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception?
: definiuje funkcję zawieszania o nazwie signIn. Oznacza to, że można go wstrzymać i wznowić bez blokowania głównego wątku.Zwraca on Exception?
, który będzie miał wartość null, jeśli logowanie się powiedzie, lub konkretny wyjątek, jeśli logowanie się nie powiedzie.
Przyjmuje 2 parametry:
request
: obiektGetCredentialRequest
, który zawiera konfigurację typu danych logowania do pobrania (np. Identyfikator Google).context
: kontekst Androida potrzebny do interakcji z systemem.
W przypadku treści funkcji:
val credentialManager = CredentialManager.create(context)
: tworzy instancję CredentialManager, która jest głównym interfejsem do interakcji z interfejsem Credential Manager API. W ten sposób aplikacja rozpocznie proces logowania.val failureMessage = "Sign in failed!"
: definiuje ciąg znaków (failureMessage), który ma być wyświetlany w powiadomieniu, gdy logowanie się nie powiedzie.var e: Exception? = null
: ten wiersz inicjuje zmienną e, która będzie przechowywać wszelkie wyjątki, jakie mogą wystąpić podczas procesu, zaczynając od wartości null.delay(250)
: wprowadza opóźnienie 250 milisekund. Jest to obejście potencjalnego problemu, w którym wyjątek NoCredentialException może być zgłaszany natychmiast po uruchomieniu aplikacji, zwłaszcza podczas korzystania z przepływu BottomSheet. Daje to systemowi czas na zainicjowanie menedżera danych logowania.try { ... } catch (e: Exception) { ... }
:Blok try-catch służy do niezawodnej obsługi błędów. Dzięki temu w przypadku wystąpienia błędu podczas procesu logowania aplikacja nie ulegnie awarii i będzie mogła prawidłowo obsłużyć wyjątek.val result = credentialManager.getCredential(request = request, context = context)
: w tym miejscu następuje rzeczywiste wywołanie interfejsu Credential Manager API, które inicjuje proces pobierania danych logowania. Przyjmuje żądanie i kontekst jako dane wejściowe i wyświetla użytkownikowi interfejs, w którym może on wybrać dane logowania. Jeśli operacja się uda, zwróci wynik zawierający wybrane dane logowania. Wynik tej operacji,GetCredentialResponse
, jest przechowywany w zmiennejresult
.Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
:wyświetla krótki komunikat informujący o udanym logowaniu.Log.i(TAG, "Sign in Successful!")
: zapisuje w logcatcie zabawną, udaną wiadomość.catch (e: GetCredentialException)
: obsługuje wyjątki typuGetCredentialException
. Jest to klasa nadrzędna dla kilku konkretnych wyjątków, które mogą wystąpić podczas pobierania danych logowania.catch (e: GoogleIdTokenParsingException)
: obsługuje wyjątki, które występują, gdy podczas analizowania tokena identyfikatora Google wystąpi błąd.catch (e: NoCredentialException)
: obsługuje wyjątekNoCredentialException
, który jest zgłaszany, gdy użytkownik nie ma dostępnych danych logowania (np. nie zapisał żadnych danych lub nie ma konta Google).- Co ważne, ta funkcja zwraca wyjątek przechowywany w
e
,NoCredentialException
, co umożliwia wywołującemu obsługę konkretnego przypadku, gdy nie są dostępne żadne dane logowania.
- Co ważne, ta funkcja zwraca wyjątek przechowywany w
catch (e: GetCredentialCustomException)
: obsługuje niestandardowe wyjątki, które mogą być zgłaszane przez dostawcę danych logowania.catch (e: GetCredentialCancellationException)
: obsługuje wyjątekGetCredentialCancellationException
, który jest zgłaszany, gdy użytkownik anuluje proces logowania.Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
: wyświetla komunikat w formie wyskakującego okienka informujący o tym, że logowanie nie powiodło się z powodu failureMessage.Log.e(TAG, "", e)
: rejestruje wyjątek w logcat Androida za pomocą Log.e, który jest używany w przypadku błędów. Będzie on zawierać ślad stosu wyjątku, co ułatwi debugowanie. Zawiera też emotikon z wyrazem złości dla zabawy.
return e
: funkcja zwraca wyjątek, jeśli został przechwycony, lub wartość null, jeśli logowanie się powiodło.
Podsumowując, ten kod umożliwia obsługę logowania użytkownika za pomocą interfejsu Credential Manager API, zarządza operacją asynchroniczną, obsługuje potencjalne błędy i przekazuje użytkownikowi informacje zwrotne za pomocą powiadomień i logów, a przy tym dodaje odrobinę humoru do obsługi błędów.
Wdrażanie przepływu planszy dolnej w aplikacji
Teraz możemy skonfigurować wywołanie, które uruchomi przepływ BottomSheet w naszej klasie MainActivity
, używając tego kodu i identyfikatora klienta aplikacji internetowej skopiowanego wcześniej z konsoli Google Cloud:
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)
}
}
}
}
}
Teraz możemy zapisać projekt (Plik > Zapisz) i go uruchomić:
- Naciśnij przycisk uruchamiania:
- Gdy aplikacja zostanie uruchomiona na emulatorze, powinien pojawić się arkusz BottomSheet logowania. Kliknij Dalej, aby przetestować logowanie.
- Powinien zostać wyświetlony komunikat informujący o zalogowaniu się.
7. Proces tworzenia przycisków
Proces logowania przez Google za pomocą przycisku ułatwia użytkownikom rejestrację i logowanie się w aplikacji na Androida przy użyciu istniejącego konta Google. Użytkownicy mogą z niej skorzystać, jeśli zamkną arkusz u dołu ekranu lub wolą wyraźnie użyć konta Google do zalogowania się lub zarejestrowania. Dla deweloperów oznacza to łatwiejsze wprowadzanie i mniej problemów podczas rejestracji.
Można to zrobić za pomocą gotowego przycisku Jetpack Compose, ale my użyjemy wstępnie zatwierdzonej ikony marki ze strony Wytyczne dotyczące marki Zaloguj się przez Google.
Dodawanie ikony marki do projektu
- Pobierz plik ZIP z wstępnie zatwierdzonymi ikonami marek tutaj
- Rozpakuj pobrany plik signin-assest.zip (sposób rozpakowywania zależy od systemu operacyjnego komputera). Teraz możesz otworzyć folder signin-assets i przejrzeć dostępne ikony. W tym ćwiczeniu użyjemy
signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png
. - Kopiowanie pliku
- Wklej do projektu w Android Studio w sekcji res > drawable, klikając prawym przyciskiem myszy folder drawable i wybierając Wklej (może być konieczne rozwinięcie folderu res, aby go zobaczyć).
- Pojawi się okno z prośbą o zmianę nazwy pliku i potwierdzenie katalogu, do którego zostanie on dodany. Zmień nazwę zasobu na siwg_button.png, a następnie kliknij OK.
Kod przepływu przycisku
Ten kod będzie używać tej samej funkcji signIn()
, która jest używana w przypadku BottomSheet()
, ale zamiast GetGoogleIdOption
będzie używać GetSignInWithGoogleOption
, ponieważ ten proces nie wykorzystuje przechowywanych na urządzeniu danych logowania ani kluczy dostępu do wyświetlania opcji logowania. Oto kod, który możesz wkleić pod funkcją 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)
)
}
Aby wyjaśnić, co robi kod:
fun ButtonUI(webClientId: String)
: deklaruje funkcję o nazwie ButtonUI
, która przyjmuje argument webClientId
(identyfikator klienta projektu Google Cloud).
val context = LocalContext.current
: pobiera bieżący kontekst Androida. Jest to wymagane w przypadku różnych operacji, w tym uruchamiania komponentów interfejsu.
val coroutineScope = rememberCoroutineScope()
: tworzy zakres współprogramu. Służy do zarządzania zadaniami asynchronicznymi, dzięki czemu kod może działać bez blokowania wątku głównego. rememberCoroutineScope
() to funkcja typu „composable” z Jetpack Compose, która zapewnia zakres powiązany z cyklem życia funkcji typu „composable”.
val onClick: () -> Unit = { ... }
: tworzy funkcję lambda, która zostanie wykonana po kliknięciu przycisku. Funkcja Lambda:
val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder(serverClientId = webClientId).setNonce(generateSecureRandomNonce()).build()
: Ta część tworzy obiektGetSignInWithGoogleOption
. Ten obiekt służy do określania parametrów procesu „Zaloguj się przez Google”. Wymaga onwebClientId
i wartości nonce (losowego ciągu znaków używanego do celów związanych z bezpieczeństwem).val request: GetCredentialRequest = GetCredentialRequest.Builder().addCredentialOption(signInWithGoogleOption).build()
: tworzy to obiektGetCredentialRequest
. Ta prośba zostanie użyta do uzyskania poświadczeń użytkownika za pomocą Menedżera poświadczeń.GetCredentialRequest
dodaje utworzony wcześniejGetSignInWithGoogleOption
jako opcję, aby poprosić o dane logowania „Zaloguj się przez Google”.
coroutineScope.launch { ... }
:CoroutineScope
do zarządzania operacjami asynchronicznymi (za pomocą korutyn).signIn(request, context)
: wywołuje zdefiniowaną wcześniej funkcjęsignIn
()
Image(...)
: renderuje obraz przy użyciu elementu painterResource
, który wczytuje obraz R.drawable.siwg_button
.
Modifier.fillMaxSize().clickable(enabled = true, onClick = onClick)
:fillMaxSize()
: powoduje wypełnienie dostępnego miejsca przez obraz.clickable(enabled = true, onClick = onClick)
: sprawia, że obraz można kliknąć, a po kliknięciu wykonuje on wcześniej zdefiniowaną funkcję lambda onClick.
Podsumowując, ten kod konfiguruje przycisk „Zaloguj się przez Google” w interfejsie Jetpack Compose. Gdy użytkownik kliknie ten przycisk, przygotowywana jest prośba o dane logowania, która uruchamia Menedżera danych logowania i umożliwia użytkownikowi zalogowanie się za pomocą konta Google.
Teraz musimy zaktualizować klasę MainActivity, aby uruchomić funkcję 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)
}
}
}
}
}
}
Teraz możemy zapisać projekt (Plik > Zapisz) i go uruchomić:
- Naciśnij przycisk uruchamiania:
- Gdy aplikacja zostanie uruchomiona na emulatorze, powinien pojawić się BottomSheet. Kliknij poza nią, aby ją zamknąć.
- W aplikacji powinien być teraz widoczny utworzony przez nas przycisk. Kliknij go, aby wyświetlić okno logowania.
- Kliknij swoje konto, aby się zalogować.
8. Podsumowanie
To ćwiczenie zostało ukończone. Więcej informacji lub pomocy dotyczących logowania się przez Google na Androidzie znajdziesz w sekcji Najczęstsze pytania poniżej:
Najczęstsze pytania
- Stackoverflow
- Przewodnik rozwiązywania problemów z Menedżerem danych logowania na Androidzie
- Najczęstsze pytania dotyczące Menedżera danych logowania na Androidzie
- Centrum pomocy dotyczące weryfikacji aplikacji OAuth
Pełny kod MainActivity.kt
Oto pełny kod pliku 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
}