איך מטמיעים את התכונה 'כניסה באמצעות חשבון Google' באפליקציית Android

1. לפני שמתחילים

ב-Codelab הזה תלמדו איך להטמיע את התכונה 'כניסה באמצעות חשבון Google' ב-Android באמצעות Credential Manager.

דרישות מוקדמות

  • הבנה בסיסית של השימוש ב-Kotlin לפיתוח ל-Android
  • הבנה בסיסית של Jetpack פיתוח נייטיב (מידע נוסף זמין כאן)

מה תלמדו

  • איך יוצרים פרויקט ב-Google Cloud
  • איך יוצרים לקוחות OAuth במסוף Google Cloud
  • איך מטמיעים את התכונה 'התחברות באמצעות חשבון Google' באמצעות תהליך גיליון תחתון
  • איך מטמיעים את התכונה 'כניסה באמצעות חשבון Google' באמצעות תהליך הכפתור

מה צריך

2. יצירת פרויקט ב-Android Studio

משך הזמן: 3:00 עד 5:00

כדי להתחיל, צריך ליצור פרויקט חדש ב-Android Studio:

  1. פתיחת Android Studio
  2. לוחצים על פרויקט חדשמסך הפתיחה של Android Studio.
  3. בוחרים באפשרות טלפון וטאבלט ואז באפשרות פעילות ריקהפרויקט Android Studio
  4. לוחצים על הבא.
  5. עכשיו הגיע הזמן להגדיר כמה חלקים בפרויקט:
    • שם: השם של הפרויקט
    • שם החבילה: השדה הזה יאוכלס אוטומטית על סמך שם הפרויקט
    • מיקום השמירה: כברירת מחדל, זה אמור להיות המיקום שבו Android Studio שומר את הפרויקטים. אפשר לשנות את המיקום הזה בכל שלב.
    • גרסת ה-SDK המינימלית: זו הגרסה הכי נמוכה של Android SDK שהאפליקציה מיועדת לפעול בה. ב-CodeLab הזה נשתמש ב-API 36 ‏ (Baklava)
    פרויקט הגדרה של Android Studio
  6. לוחצים על סיום.
  7. ‫Android Studio ייצור את הפרויקט ויוריד את כל התלויות הנדרשות לאפליקציית הבסיס. התהליך הזה יכול להימשך כמה דקות. כדי לראות את זה קורה, פשוט לוחצים על סמל הבנייה:Android Studio Project Building
  8. אחרי שהתהליך מסתיים, Android Studio אמור להיראות כך:פרויקט Android Studio נוצר

3. הגדרת הפרויקט ב-Google Cloud

יצירת פרויקט של Google Cloud

  1. כניסה למסוף Google Cloud
  2. פותחים פרויקט או יוצרים פרויקט חדשGCP create new projectGCP create new project 2GCP create new project 3
  3. לוחצים על ממשקי API ושירותיםממשקי API ושירותים של GCP.
  4. עוברים אל מסך ההסכמה ל-OAuthמסך הסכמה ל-OAuth ב-GCP
  5. כדי להמשיך, צריך למלא את השדות בדף סקירה כללית. לוחצים על שנתחיל? כדי להתחיל למלא את הפרטים הבאים:כפתור 'התחלה' ב-GCP
    • שם האפליקציה: השם של האפליקציה הזו, שצריך להיות זהה לשם שהשתמשתם בו כשפתחתם את הפרויקט ב-Android Studio.
    • כתובת אימייל לתמיכה במשתמשים: כאן יופיע חשבון Google שדרכו נכנסתם וכל קבוצות Google שאתם מנהלים.
    פרטי אפליקציה ב-GCP
    • קהל:
      • פנימיות לאפליקציה שמשמשת רק בתוך הארגון. אם אין לכם ארגון שמשויך לפרויקט Google Cloud, לא תוכלו לבחור באפשרות הזו.
      • אנחנו נשתמש באפשרות 'חיצוני'.
    קהל ב-GCP
    • פרטים ליצירת קשר: אפשר להזין כאן כל כתובת אימייל שרוצים שתשמש כנקודת הקשר לגבי הבקשה
    פרטים ליצירת קשר ב-GCP
    • קוראים את המדיניות של Google בנושא נתוני משתמשים בשירותי API.
  6. אחרי שקוראים את המדיניות בנושא נתוני משתמשים ומסכימים לה, לוחצים על יצירהיצירה ב-GCP

הגדרת לקוחות OAuth

אחרי שמגדירים פרויקט ב-Google Cloud, צריך להוסיף לקוח אינטרנט ולקוח Android כדי שנוכל לבצע קריאות ל-API לשרת הבק-אנד של OAuth באמצעות מזהי הלקוח שלהם.

כדי להשתמש בלקוח האינטרנט של Android, צריך:

  • שם החבילה של האפליקציה (למשל, com.example.example)
  • חתימת SHA-1 של האפליקציה
    • מהי חתימת SHA-1?
      • טביעת האצבע מסוג SHA-1 היא גיבוב קריפטוגרפי שנוצר ממפתח החתימה של האפליקציה. הוא משמש כמזהה ייחודי של אישור החתימה של האפליקציה הספציפית שלכם. אפשר לחשוב על זה כמו על 'חתימה' דיגיטלית לאפליקציה.
    • למה אנחנו צריכים את חתימת SHA-1?
      • טביעת האצבע של SHA-1 מבטיחה שרק האפליקציה שלכם, שנחתמה באמצעות מפתח החתימה הספציפי שלכם, יכולה לבקש טוקנים לגישה באמצעות מזהה הלקוח שלכם ב-OAuth 2.0. כך נמנעת גישה של אפליקציות אחרות (גם כאלה עם אותו שם חבילה) למשאבים ולנתוני המשתמשים של הפרויקט.
      • אפשר לחשוב על זה כך:
        • מפתח החתימה של האפליקציה הוא כמו המפתח הפיזי ל'דלת' של האפליקציה. היא מאפשרת גישה לפעולות הפנימיות של האפליקציה.
        • טביעת האצבע של SHA-1 היא כמו מזהה ייחודי של כרטיס מפתח שמקושר למפתח הפיזי שלכם. זהו קוד ספציפי שמזהה את המפתח המסוים הזה.
        • מזהה לקוח OAuth 2.0 הוא כמו קוד כניסה למשאב או לשירות ספציפיים של Google (למשל, כניסה באמצעות חשבון Google).
        • כשמספקים את טביעת האצבע של SHA-1 במהלך הגדרת לקוח OAuth, בעצם אומרים ל-Google: "רק כרטיס המפתח עם המזהה הספציפי הזה (SHA-1) יכול לפתוח את קוד הגישה הזה (מזהה הלקוח)". כך מוודאים שרק האפליקציה שלכם יכולה לגשת לשירותי Google שמקושרים לקוד הכניסה הזה".

במקרה של לקוח אינטרנט, כל מה שצריך הוא השם שבו רוצים להשתמש כדי לזהות את הלקוח במסוף.

יצירת לקוח OAuth 2.0 ל-Android

  1. עוברים לדף לקוחותלקוחות GCP
  2. לוחצים על Create Client (יצירת לקוח)GCP Create Clients.
  3. בקטע Application type (סוג האפליקציה), בוחרים באפשרות Android.
  4. תצטרכו לציין את שם החבילה של האפליקציה
  5. מ-Android Studio, צריך לקבל את חתימת ה-SHA-1 של האפליקציה ולהעתיק אותה לכאן:
    1. עוברים אל Android Studio ופותחים את הטרמינל.
    2. מריצים את הפקודה הבאה: Mac/Linux:
      keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
      
      Windows:
        keytool -list -v -keystore "C:\Users\USERNAME\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android
      
      הפקודה הזו נועדה להציג את הפרטים של רשומה ספציפית (כינוי) במאגר מפתחות.
      • -list: האפשרות הזו מורה ל-keytool להציג את התוכן של מאגר המפתחות.
      • -v: האפשרות הזו מאפשרת פלט מפורט, ומספקת מידע מפורט יותר על הרשומה.
      • -keystore ~/.android/debug.keystore: כאן מציינים את הנתיב לקובץ של מאגר המפתחות.
      • -alias androiddebugkey: כאן מציינים את הכינוי (שם הרשומה) של המפתח שרוצים לבדוק.
      • -storepass android: כאן מציינים את הסיסמה לקובץ מאגר המפתחות.
      • -keypass android: הסיסמה של המפתח הפרטי של הכינוי שצוין.
    3. מעתיקים את הערך של חתימת SHA-1:
    חתימת SHA
    1. חוזרים לחלון Google Cloud ומדביקים את ערך חתימת ה-SHA-1:
  6. המסך שלכם אמור להיראות עכשיו כמו בתמונה הבאה, ואפשר ללחוץ על יצירה:פרטי לקוח Androidלקוח Android

יצירת לקוח OAuth 2.0 לאינטרנט

  1. כדי ליצור מזהה לקוח של אפליקציית אינטרנט, חוזרים על שלבים 1-2 מהקטע יצירת לקוח Android ובוחרים באפשרות אפליקציית אינטרנט בשדה 'סוג האפליקציה'.
  2. נותנים ללקוח שם (זה יהיה לקוח OAuth): פרטי לקוח האינטרנט
  3. לוחצים על יצירהלקוח אינטרנט.
  4. מעתיקים את מזהה הלקוח מהחלון הקופץ, הוא יידרש בהמשךהעתקת ה-Client ID

עכשיו, אחרי שהגדרנו את כל לקוחות ה-OAuth, אפשר לחזור ל-Android Studio וליצור את אפליקציית Android 'כניסה באמצעות חשבון Google'.

4. הגדרה של מכשיר אנדרואיד וירטואלי

כדי לבדוק את האפליקציה במהירות בלי מכשיר Android פיזי, כדאי ליצור מכשיר Android וירטואלי שבו אפשר לבנות את האפליקציה ולהריץ אותה באופן מיידי מ-Android Studio. אם רוצים לבצע בדיקה באמצעות מכשיר Android פיזי, אפשר לפעול לפי ההוראות שמפורטות במסמכים למפתחי Android.

יצירת מכשיר וירטואלי של Android

  1. ב-Android Studio, פותחים את Device Managerניהול מכשיר
  2. לוחצים על הלחצן + > יצירת מכשיר וירטואלייצירת מכשיר וירטואלי
  3. מכאן אפשר להוסיף כל מכשיר שצריך לפרויקט. למטרות של Codelab הזה, בוחרים באפשרות Medium Phone (טלפון בינוני) ולוחצים על Next (הבא)טלפון בינוני.
  4. עכשיו אפשר להגדיר את המכשיר לפרויקט על ידי מתן שם ייחודי, בחירת גרסת Android שתופעל במכשיר ועוד. מוודאים שממשק ה-API מוגדר ל-API 36 "Baklava"; Android 16 ואז לוחצים על סיוםהגדרת מכשיר וירטואלי
  5. המכשיר החדש אמור להופיע ב-Device Manager. כדי לוודא שהמכשיר פועל, לוחצים על הפעלת המכשיר לצד המכשיר שיצרתםהפעלת מכשיר 2.
  6. המכשיר אמור לפעול עכשיו.מכשיר שבו פועלת אפליקציה

כניסה למכשיר Android וירטואלי

המכשיר שיצרתם עובד. כדי למנוע שגיאות כשבודקים את התכונה 'כניסה באמצעות חשבון Google', צריך להיכנס למכשיר באמצעות חשבון Google.

  1. עוברים להגדרות:
    1. לוחצים במרכז המסך במכשיר הווירטואלי ומחליקים למעלה.
    לחיצה והחלקה
    1. מחפשים את אפליקציית ההגדרות ולוחצים עליה.
    אפליקציית ההגדרות
  2. לוחצים על Google בהגדרותשירותי Google והעדפות
  3. לוחצים על כניסה ופועלים לפי ההוראות כדי להיכנס לחשבון Googleכניסה לחשבון במכשיר.
  1. עכשיו אתם אמורים להיות מחוברים לחשבון במכשירהמכשיר מחובר לחשבון

המכשיר הווירטואלי שלך עם Android מוכן עכשיו לבדיקה.

5. הוספת יחסי תלות

משך 5:00

כדי לבצע קריאות ל-OAuth API, קודם צריך לשלב את הספריות הנדרשות שמאפשרות לנו לבצע בקשות אימות ולהשתמש במזהי Google כדי לבצע את הבקשות האלה:

  • libs.googleid
  • libs.play.services.auth
  1. עוברים אל File > Project Structure (קובץ > מבנה הפרויקט):מבנה הפרויקט
  2. אחר כך עוברים אל Dependencies (תלויות) > app (אפליקציה) > '+' > Library Dependency (תלות בספרייה)תלויות
  3. עכשיו צריך להוסיף את הספריות:
    1. בתיבת הדו-שיח של החיפוש, מקלידים googleid ולוחצים על חיפוש.
    2. צריכה להיות רק רשומה אחת. בוחרים אותה ואת הגרסה הכי גבוהה שזמינה (בזמן יצירת ה-Codelab הזה, הגרסה היא 1.1.1).
    3. לוחצים על אישורחבילת מזהים של Google
    4. חוזרים על שלבים 1-3, אבל הפעם מחפשים את play-services-auth ובוחרים בשורה עם com.google.android.gms בתור Group ID ו-play-services-auth בתור Artifact Nameאימות ב-Play Services
  4. לוחצים על אישורFinished Dependencies

6. תהליך הגיליון התחתון

תהליך הגיליון התחתון

תהליך ההרשמה באמצעות דף התחתון מבוסס על Credential Manager API, ומאפשר למשתמשים להיכנס לאפליקציה באמצעות חשבונות Google שלהם ב-Android בצורה פשוטה ויעילה. הוא נועד להיות מהיר ונוח, במיוחד למשתמשים חוזרים. התהליך הזה צריך להיות מופעל כשפותחים את האפליקציה.

יצירת בקשת הכניסה

  1. כדי להתחיל, צריך להסיר את הפונקציות Greeting() ו-GreetingPreview() מה-MainActivity.kt, כי לא נשתמש בהן.
  2. עכשיו צריך לוודא שהחבילות שאנחנו צריכים מיובאות לפרויקט הזה. אפשר להוסיף את משפטי import הבאים אחרי המשפטים הקיימים, החל משורה 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
    
  3. בשלב הבא, צריך ליצור את הפונקציה לבניית הבקשה של התחתית. מדביקים את הקוד הזה מתחת למחלקה 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 פיתוח נייטיב שמאפשר להפעיל פונקציית השעיה (פונקציה שיכולה להשהות ולחדש את ההרצה) במהלך מחזור החיים של הקומפוזבילי. הערך Unit כמפתח אומר שהאפקט הזה יפעל רק פעם אחת כשהקומפוזיציה תופעל בפעם הראשונה.
    • val googleIdOption: GetGoogleIdOption = ...: יצירת אובייקט GetGoogleIdOption. האובייקט הזה מגדיר את סוג פרטי הכניסה שמבוקשים מ-Google.
      • .Builder(): נעשה שימוש בתבנית builder כדי להגדיר את האפשרויות.
      • .setFilterByAuthorizedAccounts(true): מציין אם לאפשר למשתמש לבחור מתוך כל חשבונות Google, או רק מתוך החשבונות שכבר אישרו את האפליקציה. במקרה הזה, הערך מוגדר כ-true, כלומר הבקשה תתבצע באמצעות פרטי הכניסה שהמשתמש אישר בעבר לשימוש באפליקציה הזו, אם יש כאלה.
      • .setServerClientId(webClientId): הגדרת מזהה הלקוח של השרת, שהוא מזהה ייחודי עבור הקצה העורפי של האפליקציה. הפעולה הזו נדרשת כדי לקבל אסימון מזהה.
      • .setNonce(generateSecureRandomNonce()): הגדרת ערך אקראי (nonce) כדי למנוע התקפות שליחה מחדש ולוודא שאסימון המזהה משויך לבקשה הספציפית.
      • .build(): יוצר את אובייקט GetGoogleIdOption עם ההגדרות שצוינו.
    • val request: GetCredentialRequest = ...: יצירת אובייקט GetCredentialRequest. האובייקט הזה מכיל את כל בקשת פרטי הכניסה.
      • .Builder(): מתחיל את תבנית ה-builder להגדרת הבקשה.
      • .addCredentialOption(googleIdOption): מוסיף את googleIdOption לבקשה, ומציין שאנחנו רוצים לבקש אסימון מזהה של Google.
      • .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 = ...: נוצר GetCredentialRequest חדש עם googleIdOptionFalse.
      • signIn(requestFalse, context): מתבצע ניסיון להכניס את המשתמש באמצעות הבקשה החדשה שמאפשרת שימוש בכל חשבון.

במהות, הקוד הזה מכין בקשה ל-Credential Manager API כדי לאחזר אסימון מזהה של Google עבור המשתמש, באמצעות ההגדרות שסופקו. אחר כך אפשר להשתמש ב-GetCredentialRequest כדי להפעיל את ממשק המשתמש של מנהל פרטי הכניסה, שבו המשתמש יכול לבחור את חשבון Google שלו ולהעניק את ההרשאות הנדרשות.

fun generateSecureRandomNonce(byteLength: Int = 32): String: הפונקציה generateSecureRandomNonce מוגדרת. היא מקבלת ארגומנט של מספר שלם byteLength (עם ערך ברירת מחדל של 32) שמציין את האורך הרצוי של ה-nonce בבייטים. הפונקציה מחזירה מחרוזת, שהיא הייצוג בקידוד Base64 של הבייטים האקראיים.

  • val randomBytes = ByteArray(byteLength): יוצר מערך בייטים עם byteLength שצוין כדי להכיל את הבייטים האקראיים.
  • SecureRandom.getInstanceStrong().nextBytes(randomBytes):
    • SecureRandom.getInstanceStrong(): הפונקציה הזו מקבלת מחולל מספרים אקראיים חזק מבחינה קריפטוגרפית. הדבר חשוב מאוד לאבטחה, כי הוא מבטיח שהמספרים שנוצרו הם אקראיים באמת ולא צפויים. הוא משתמש במקור האנטרופיה החזק ביותר שזמין במערכת.
    • .nextBytes(randomBytes): הפקודה הזו מאכלסת את המערך randomBytes בבייטים אקראיים שנוצרו על ידי המופע SecureRandom.
  • return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes):
    • Base64.getUrlEncoder(): הפונקציה הזו מקבלת מקודד Base64 שמשתמש באלפבית בטוח לכתובות URL (משתמש ב-‎-‎ וב-‎_‎ במקום ב-‎+‎ וב-‎/‎). זה חשוב כי זה מבטיח שאפשר להשתמש בבטחה במחרוזת שמתקבלת בכתובות URL בלי צורך בקידוד נוסף.
    • .withoutPadding(): הפונקציה הזו מסירה את תווי הריפוד ממחרוזת בקידוד Base64. לרוב, כדאי לעשות את זה כדי שהערך של ה-nonce יהיה קצר וקומפקטי יותר.
    • .encodeToString(randomBytes): הפונקציה הזו מקודדת את ה-randomBytes למחרוזת Base64 ומחזירה אותה.

לסיכום, הפונקציה הזו יוצרת ערך חד-פעמי אקראי חזק מבחינה קריפטוגרפית באורך שצוין, מקודדת אותו באמצעות Base64 בטוח לשימוש בכתובות URL ומחזירה את המחרוזת שמתקבלת. זוהי שיטה סטנדרטית ליצירת ערכי nonce שניתן להשתמש בהם בבטחה בהקשרים שבהם האבטחה חשובה.

שליחת בקשת הכניסה

עכשיו, אחרי שיצרנו את בקשת הכניסה, אנחנו יכולים להשתמש ב-Credential Manager כדי להיכנס לחשבון. לשם כך, אנחנו צריכים ליצור פונקציה שמטפלת בהעברת בקשות כניסה באמצעות 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?, שיהיה null אם הכניסה לחשבון תצליח, או את החריגה הספציפית אם הכניסה תיכשל.

הוא כולל שני פרמטרים:

  • request: אובייקט GetCredentialRequest שמכיל את ההגדרה של סוג פרטי הכניסה לשליפה (למשל, מזהה Google).
  • context: ההקשר של Android שנדרש כדי ליצור אינטראקציה עם המערכת.

בגוף הפונקציה:

  • val credentialManager = CredentialManager.create(context): יוצר מופע של CredentialManager, שהוא הממשק הראשי לאינטראקציה עם Credential Manager API. כך האפליקציה תתחיל את תהליך הכניסה.
  • val failureMessage = "Sign in failed!": מגדיר מחרוזת (failureMessage) שתוצג בהודעה קופצת כשהכניסה נכשלת.
  • var e: Exception? = null: השורה הזו מאתחלת משתנה e כדי לאחסן חריג כלשהו שעשוי להתרחש במהלך התהליך, החל מ-null.
  • delay(250): מוסיף השהיה של 250 אלפיות השנייה. זהו פתרון זמני לבעיה פוטנציאלית שבה יכול להיות שיוחזר NoCredentialException מיד כשהאפליקציה מופעלת, במיוחד כשמשתמשים בתהליך BottomSheet. כך המערכת מקבלת זמן לאתחל את מנהל פרטי הכניסה.
  • 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.
    • 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): השגיאה מתועדת ב-Logcat של Android באמצעות Log.e, שמשמש לתיעוד שגיאות. הדיווח יכלול את ה-stacktrace של החריג כדי לעזור בניפוי הבאגים. הוא כולל גם את האמוטיקון הכועס בשביל הכיף.
  • return e: הפונקציה מחזירה את החריגה אם נתפסה חריגה כלשהי, או null אם הכניסה הצליחה.

לסיכום, הקוד הזה מספק דרך לטפל בכניסה של משתמשים באמצעות Credential Manager API, מנהל את הפעולה האסינכרונית, מטפל בשגיאות פוטנציאליות ומספק למשתמש משוב באמצעות הודעות קצרות ויומנים, תוך הוספת הומור לטיפול בשגיאות.

הטמעה של תהליך הגיליון התחתון באפליקציה

עכשיו אפשר להגדיר קריאה להפעלת התהליך של BottomSheet במחלקה MainActivity באמצעות הקוד הבא ומזהה הלקוח של אפליקציית האינטרנט שהעתקנו קודם מ-מסוף 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)
                }
            }
        }
    }
}

עכשיו אפשר לשמור את הפרויקט (File > ‏Save) ולהפעיל אותו:

  1. לוחצים על לחצן ההפעלה:הפעלת הפרויקט
  2. אחרי שהאפליקציה תפעל באמולטור, אמור להופיע חלון קופץ של BottomSheet לכניסה לחשבון. לוחצים על המשך כדי לבדוק את הכניסהגיליון תחתון
  3. אמורה להופיע הודעה קופצת שמאשרת שהכניסה לחשבון בוצעה בהצלחה.גיליון תחתון של הצלחה

7. תזרים לחצנים

קובץ GIF של זרימת לחצנים

התהליך של הכפתור 'כניסה באמצעות חשבון Google' מקל על המשתמשים להירשם לאפליקציית Android שלכם או להתחבר אליה באמצעות חשבון Google הקיים שלהם. הם ילחצו על הלחצן הזה אם הם יסגרו את הגיליון התחתון או אם הם פשוט יעדיפו להשתמש בחשבון Google שלהם כדי להיכנס או להירשם. למפתחים, המשמעות היא תהליך הצטרפות חלק יותר ופחות חיכוכים במהלך ההרשמה.

אפשר לעשות את זה באמצעות לחצן Jetpack פיתוח נייטיב מוכן מראש, אבל אנחנו נשתמש בסמל מותג שאושר מראש מתוך הדף הנחיות למיתוג של 'כניסה באמצעות חשבון Google'.

הוספת סמל מותג לפרויקט

  1. כאן אפשר להוריד קובץ ZIP של סמלי מותגים שאושרו מראש
  2. מבטלים את הדחיסה של הקובץ signin-assest.zip מההורדות (הפעולה הזו משתנה בהתאם למערכת ההפעלה של המחשב). עכשיו אפשר לפתוח את התיקייה signin-assets ולעיין בסמלים הזמינים. ב-Codelab הזה נשתמש ב-signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png.
  3. העתקת הקובץ
  4. מדביקים את הקובץ בפרויקט ב-Android Studio בקטע res > ‏drawable. לשם כך, לוחצים לחיצה ימנית על התיקייה drawable ואז על Paste (הדבק). יכול להיות שתצטרכו להרחיב את התיקייה res כדי לראות אותה.פריט גרפי שניתן להזזה
  5. תופיע תיבת דו-שיח עם בקשה לשנות את שם הקובץ ולאשר את הספרייה שאליה הוא יתווסף. משנים את השם של הנכס ל-siwg_button.png ולוחצים על אישורהוספת כפתור

קוד של זרימת כפתור

הקוד הזה ישתמש באותה פונקציה signIn() שמשמשת לBottomSheet(), אבל ישתמש ב-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).

val context = LocalContext.current: מאחזר את ההקשר הנוכחי של Android. הפעולה הזו נדרשת לפעולות שונות, כולל הפעלת רכיבי ממשק משתמש.

val coroutineScope = rememberCoroutineScope(): יוצרת היקף של שגרת משנה. הוא משמש לניהול משימות אסינכרוניות, ומאפשר לקוד לפעול בלי לחסום את ה-thread הראשי. ‫rememberCoroutineScope() היא פונקציה הניתנת להגדרה מ-Jetpack פיתוח נייטיב שמספקת היקף שקשור למחזור החיים של הרכיב הקומפוזבילי.

val onClick: () -> Unit = { ... }: הפקודה הזו יוצרת פונקציית lambda שתופעל כשלוחצים על הכפתור. פונקציית הלמדה:

  • val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder(serverClientId = webClientId).setNonce(generateSecureRandomNonce()).build(): בחלק הזה נוצר אובייקט GetSignInWithGoogleOption. האובייקט הזה משמש לציון הפרמטרים של התהליך 'כניסה באמצעות חשבון Google'. הוא דורש את הפרמטר webClientId ואת הערך nonce (מחרוזת אקראית שמשמשת לאבטחה).
  • val request: GetCredentialRequest = GetCredentialRequest.Builder().addCredentialOption(signInWithGoogleOption).build(): הפקודה הזו יוצרת אובייקט GetCredentialRequest. הבקשה הזו תשמש לקבלת פרטי הכניסה של המשתמש באמצעות Credential Manager. ה-GetCredentialRequest מוסיף את GetSignInWithGoogleOption שנוצר קודם כאפשרות, כדי לבקש אישור כניסה באמצעות חשבון Google.
  • coroutineScope.launch { ... }: CoroutineScope לניהול פעולות אסינכרוניות (באמצעות קורוטינות).
    • signIn(request, context): קריאה לפונקציה signIn() שהוגדרה קודם

Image(...): התג הזה מעבד תמונה באמצעות התג painterResource שמעלה את התמונה R.drawable.siwg_button

  • Modifier.fillMaxSize().clickable(enabled = true, onClick = onClick):
    • fillMaxSize(): התמונה תתפוס את כל השטח הזמין.
    • clickable(enabled = true, onClick = onClick): הופך את התמונה לקליקבילית, וכשלוקחים את העכבר מעל התמונה ולוחצים עליה, מופעלת פונקציית ה-lambda של onClick שהוגדרה קודם.

לסיכום, הקוד הזה מגדיר לחצן 'כניסה באמצעות חשבון Google' בממשק משתמש של Jetpack פיתוח נייטיב. כשלוחצים על הלחצן, המערכת מכינה בקשה לפרטי כניסה כדי להפעיל את Credential Manager ולאפשר למשתמש להיכנס באמצעות חשבון 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)
                    }
                }
            }
        }
    }
}

עכשיו אפשר לשמור את הפרויקט (File > ‏Save) ולהפעיל אותו:

  1. לוחצים על לחצן ההפעלה:הפעלת הפרויקט
  2. אחרי שהאפליקציה תפעל באמולטור, ה-BottomSheet אמור להופיע. כדי לסגור אותו, לוחצים מחוץ לו.לחצו כאן
  3. עכשיו אמור להופיע באפליקציה הלחצן שיצרנו. לוחצים עליו כדי לראות את תיבת הדו-שיח לכניסהתיבת דו-שיח לכניסה
  4. לוחצים על החשבון כדי להיכנס.

8. סיכום

סיימתם את ה-Codelab הזה! מידע נוסף או עזרה בנושא כניסה באמצעות חשבון Google ב-Android זמינים בקטע השאלות הנפוצות שבהמשך:

שאלות נפוצות

הקוד המלא של 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
}