מתן הרשאה לגישה לנתוני משתמשים ב-Google

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

קריאות לאימות ולהרשאה חייבות להיות שני תהליכים נפרדים ושונים, בהתאם לצרכים של האפליקציה.

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

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

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

לאימות, מומלץ להשתמש ב-Credential Manager API. כדי לאשר פעולות שדורשות גישה לנתוני משתמשים שמאוחסנים ב-Google, מומלץ להשתמש ב-AuthorizationClient.

הגדרת הפרויקט במסוף Google Cloud

  1. פותחים את הפרויקט במסוף Cloud, או יוצרים פרויקט אם עדיין אין לכם פרויקט.
  2. בדף המיתוג, מוודאים שכל המידע מלא ומדויק.
    1. מוודאים שהשם, הלוגו ודף הבית של האפליקציה נכונים. הערכים האלה יוצגו למשתמשים במסך ההסכמה של "כניסה באמצעות חשבון Google" בזמן ההרשמה ובמסך האפליקציות והשירותים של צד שלישי.
    2. מוודאים שציינתם את כתובות ה-URL של מדיניות הפרטיות ושל התנאים וההגבלות של האפליקציה.
  3. אם עדיין אין לכם מזהה לקוח של Android לאפליקציה, צרו אחד בדף Clients. תצטרכו לציין את שם החבילה של האפליקציה ואת חתימת ה-SHA-1.
    1. עוברים אל הדף "לקוחות".
    2. לוחצים על Create client.
    3. בוחרים בסוג האפליקציה Android.
    4. מזינים שם ללקוח OAuth. השם הזה מוצג בדף Clients של הפרויקט כדי לזהות את הלקוח.
    5. מזינים את שם החבילה של אפליקציית Android. הערך הזה מוגדר במאפיין package של הרכיב <manifest> בקובץ AndroidManifest.xml.
    6. מזינים את טביעת האצבע לאישור החתימה מסוג SHA-1 של הפצת האפליקציה.
    7. אם האפליקציה משתמשת בחתימת אפליקציות ב-Google Play, מעתיקים את טביעת האצבע מסוג SHA-1 מדף חתימת האפליקציה ב-Play Console.
    8. אם אתם מנהלים את מאגר המפתחות ומפתחות החתימה שלכם, השתמשו בכלי השירות keytool שכלול ב-Java כדי להדפיס את פרטי האישור בפורמט קריא לאנשים. מעתיקים את הערך של SHA-1 בקטע Certificate fingerprints של הפלט של keytool. מידע נוסף זמין במאמר אימות הלקוח במסמכי התיעוד של Google APIs ל-Android.
    9. (אופציונלי) מאמתים את הבעלות על האפליקציה ל-Android.
  4. בדף Clients, יוצרים מזהה לקוח חדש מסוג אפליקציית אינטרנט, אם עדיין לא עשיתם את זה. אתם יכולים להתעלם בינתיים מהשדות Authorized JavaScript Origins ו-Authorized redirect URIs. מזהה הלקוח הזה ישמש לזיהוי שרת בק-אנד שלכם כשהוא יתקשר עם שירותי האימות של Google.
    1. עוברים אל הדף "לקוחות".
    2. לוחצים על Create client.
    3. בוחרים בסוג אפליקציית אינטרנט.

אימות הבעלות על האפליקציה

כדי לצמצם את הסיכון להתחזות לאפליקציה, אתם יכולים לאמת את הבעלות על האפליקציה.

כדי להשלים את תהליך האימות, אתם יכולים להשתמש בחשבון הפיתוח ב-Google Play, אם יש לכם חשבון כזה והאפליקציה שלכם רשומה ב-Google Play Console. כדי שהאימות יצליח, צריך לעמוד בדרישות הבאות:

  • צריכה להיות לכם אפליקציה רשומה ב-Google Play Console עם אותו שם חבילה ואותה טביעת אצבע לאישור חתימה מסוג SHA-1 כמו של לקוח Android OAuth שעבורו אתם משלימים את האימות.
  • צריכה להיות לכם הרשאת אדמין באפליקציה ב-Google Play Console. מידע נוסף על ניהול הרשאות גישה ב-Google Play Console.

בקטע אימות בעלות על אפליקציה בלקוח Android, לוחצים על הלחצן אימות בעלות כדי להשלים את תהליך האימות.

אם האימות יצליח, תוצג הודעה שתאשר את הצלחת תהליך האימות. אחרת, תוצג הודעת שגיאה.

אם האימות נכשל, נסו את הפתרונות הבאים:

  • מוודאים שהאפליקציה שמאמתים רשומה ב-Google Play Console.
  • מוודאים שיש לכם הרשאת אדמין לאפליקציה ב-Google Play Console.

הצהרה על יחסי תלות

בקובץ build.gradle של המודול, מצהירים על יחסי תלות באמצעות הגרסה העדכנית של ספריית Google Identity Services.

dependencies {
  // ... other dependencies

  implementation "com.google.android.gms:play-services-auth:22.0.0"
}

בקשת הרשאות שנדרשות לפעולות של משתמשים

בכל פעם שמשתמש מבצע פעולה שדורשת היקף נוסף, צריך להפעיל את AuthorizationClient.authorize(). לדוגמה, אם משתמש מבצע פעולה שדורשת גישה לאחסון של אפליקציית Drive, צריך לבצע את הפעולות הבאות:

Kotlin

val requestedScopes: List<Scope> = listOf(DriveScopes.DRIVE_FILE)
val authorizationRequest = AuthorizationRequest.builder()
    .setRequestedScopes(requestedScopes)
    .build()

Identity.getAuthorizationClient(activity)
    .authorize(authorizationRequestBuilder.build())
    .addOnSuccessListener { authorizationResult ->
        if (authorizationResult.hasResolution()) {
            val pendingIntent = authorizationResult.pendingIntent
            // Access needs to be granted by the user
            startAuthorizationIntent.launch(IntentSenderRequest.Builder(pendingIntent!!.intentSender).build())
        } else {
            // Access was previously granted, continue with user action
            saveToDriveAppFolder(authorizationResult);
        }
    }
    .addOnFailureListener { e -> Log.e(TAG, "Failed to authorize", e) }

Java

List<Scopes> requestedScopes = Arrays.asList(DriveScopes.DRIVE_FILE);
AuthorizationRequest authorizationRequest = AuthorizationRequest.builder()
    .setRequestedScopes(requestedScopes)
    .build();

Identity.getAuthorizationClient(activity)
    .authorize(authorizationRequest)
    .addOnSuccessListener(authorizationResult -> {
        if (authorizationResult.hasResolution()) {
            // Access needs to be granted by the user
            startAuthorizationIntent.launch(
                new IntentSenderRequest.Builder(
                    authorizationResult.getPendingIntent().getIntentSender()
                ).build()
            );
        } else {
            // Access was previously granted, continue with user action
            saveToDriveAppFolder(authorizationResult);
        }
    })
    .addOnFailureListener(e -> Log.e(TAG, "Failed to authorize", e));

כשמגדירים את ActivityResultLauncher, צריך לטפל בתגובה כמו שמוצג בקטע הקוד הבא, שבו אנחנו מניחים שהפעולה מתבצעת ב-fragment. הקוד בודק שההרשאות הנדרשות ניתנו, ואז מבצע את הפעולה של המשתמש.

Kotlin

private lateinit var startAuthorizationIntent: ActivityResultLauncher<IntentSenderRequest>

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?,
): View? {
    // ...
    startAuthorizationIntent =
        registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { activityResult ->
            try {
                // extract the result
                val authorizationResult = Identity.getAuthorizationClient(requireContext())
                    .getAuthorizationResultFromIntent(activityResult.data)
                // continue with user action
                saveToDriveAppFolder(authorizationResult);
            } catch (e: ApiException) {
                // log exception
            }
        }
}

Java

private ActivityResultLauncher<IntentSenderRequest> startAuthorizationIntent;

@Override
public View onCreateView(
    @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// ...
startAuthorizationIntent =
    registerForActivityResult(
        new ActivityResultContracts.StartIntentSenderForResult(),
        activityResult -> {
            try {
            // extract the result
            AuthorizationResult authorizationResult =
                Identity.getAuthorizationClient(requireActivity())
                    .getAuthorizationResultFromIntent(activityResult.getData());
            // continue with user action
            saveToDriveAppFolder(authorizationResult);
            } catch (ApiException e) {
            // log exception
            }
        });
}

אם אתם ניגשים ל-Google APIs בצד השרת, צריך להפעיל את ה-method‏ getServerAuthCode() מ-AuthorizationResult כדי לקבל קוד הרשאה, שאותו אתם שולחים לבק-אנד כדי להמיר אותו לטוקן גישה ולטוקן רענון. מידע נוסף זמין במאמר שמירה על גישה קבועה לנתוני המשתמש.

ביטול הרשאות לנתונים או למשאבים של משתמשים

כדי לבטל גישה שניתנה בעבר, צריך להפעיל את AuthorizationClient.revokeAccess(). לדוגמה, אם המשתמש מסיר את החשבון שלו מהאפליקציה, ולאפליקציה ניתנה בעבר גישה אל DriveScopes.DRIVE_FILE, צריך להשתמש בקוד הבא כדי לבטל את הגישה:

Kotlin

val requestedScopes: MutableList<Scope> = mutableListOf(DriveScopes.DRIVE_FILE)
RevokeAccessRequest revokeAccessRequest = RevokeAccessRequest.builder()
    .setAccount(account)
    .setScopes(requestedScopes)
    .build()

Identity.getAuthorizationClient(activity)
    .revokeAccess(revokeAccessRequest)
    .addOnSuccessListener { Log.i(TAG, "Successfully revoked access") }
    .addOnFailureListener { e -> Log.e(TAG, "Failed to revoke access", e) }

Java

List<Scopes> requestedScopes = Arrays.asList(DriveScopes.DRIVE_FILE);
RevokeAccessRequest revokeAccessRequest = RevokeAccessRequest.builder()
    .setAccount(account)
    .setScopes(requestedScopes)
    .build();

Identity.getAuthorizationClient(activity)
    .revokeAccess(revokeAccessRequest)
    .addOnSuccessListener(unused -> Log.i(TAG, "Successfully revoked access"))
    .addOnFailureListener(e -> Log.e(TAG, "Failed to revoke access", e));

ניקוי מטמון הטוקנים

טוקני הגישה של OAuth נשמרים במטמון המקומי אחרי שהם מתקבלים מהשרת, כדי לזרז את הגישה ולצמצם את הקריאות לרשת. האסימונים האלה נמחקים אוטומטית מהמטמון כשהתוקף שלהם פג, אבל הם יכולים להפוך ללא חוקיים גם מסיבות אחרות. אם אתם מקבלים את השגיאה IllegalStateException כשאתם משתמשים בטוקן, נקו את המטמון המקומי כדי לוודא שבקשת ההרשאה הבאה לטוקן גישה תעבור לשרת OAuth. קטע הקוד הבא מסיר את invalidAccessToken מהמטמון המקומי:

Kotlin

Identity.getAuthorizationClient(activity)
    .clearToken(ClearTokenRequest.builder().setToken(invalidAccessToken).build())
    .addOnSuccessListener { Log.i(TAG, "Successfully removed the token from the cache") }
    .addOnFailureListener{ e -> Log.e(TAG, "Failed to clear token", e) }

Java

Identity.getAuthorizationClient(activity)
    .clearToken(ClearTokenRequest.builder().setToken(invalidAccessToken).build())
    .addOnSuccessListener(unused -> Log.i(TAG, "Successfully removed the token from the cache"))
    .addOnFailureListener(e -> Log.e(TAG, "Failed to clear the token cache", e));

קבלת פרטי משתמש במהלך ההרשאה

תגובת ההרשאה לא מכילה מידע על חשבון המשתמש שבו נעשה שימוש, אלא רק אסימון להיקפים המבוקשים. לדוגמה, התגובה לקבלת אסימון גישה ל-Google Drive של המשתמש לא חושפת את זהות החשבון שהמשתמש בחר, למרות שאפשר להשתמש בה כדי לגשת לקבצים ב-Drive של המשתמש. כדי לקבל מידע כמו שם המשתמש או כתובת האימייל, יש לכם את האפשרויות הבאות:

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

  • בבקשת ההרשאה, בנוסף להיקפים שאתם רוצים (לדוגמה, Drive scope), בקשו את ההיקפים userinfo,‏ profile ו-openid. אחרי שמוחזר טוקן גישה, מקבלים את פרטי המשתמש על ידי שליחת בקשת HTTP מסוג GET לנקודת הקצה של פרטי המשתמש ב-OAuth‏ (https://www.googleapis.com/oauth2/v3/userinfo) באמצעות ספריית ה-HTTP המועדפת עליכם, וכוללים את טוקן הגישה שקיבלתם בכותרת, באופן ששווה ערך לפקודת curl הבאה:

    curl -X GET \ "https://www.googleapis.com/oauth2/v1/userinfo?alt=json" \ -H "Authorization: Bearer $TOKEN"
    

    התגובה היא UserInfo, שמוגבל להיקפים שהתבקשו, בפורמט JSON.

הרשאה מחשבון שהוא לא חשבון ברירת המחדל

אם אתם משתמשים ב-Credential Manager לאימות ומריצים את AuthorizationClient.authorize(), חשבון ברירת המחדל של האפליקציה מוגדר לחשבון שהמשתמש בחר. כלומר, כל בקשות ההרשאה הבאות ישתמשו בחשבון ברירת המחדל הזה. כדי לאלץ את הצגת בורר החשבונות, מוציאים את המשתמש מהאפליקציה באמצעות ה-API‏ clearCredentialState() מ-Credential Manager.

לשמור על גישה קבועה לנתוני המשתמש

אם אתם צריכים לגשת לנתוני המשתמש מהאפליקציה, קוראים ל-AuthorizationClient.authorize() פעם אחת. בסשנים הבאים, כל עוד המשתמש לא הסיר את ההרשאות שניתנו, קוראים לאותה שיטה כדי לקבל אסימון גישה להשגת היעדים שלכם, בלי אינטראקציה עם המשתמש. לעומת זאת, אם אתם צריכים לגשת לנתוני המשתמש במצב אופליין, משרת הבק-אנד שלכם, אתם צריכים לבקש סוג אחר של טוקן שנקרא "טוקן רענון".

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

כדי לקבל טוקן רענון, קודם צריך לקבל קוד אימות (או קוד הרשאה) במהלך שלב ההרשאה באפליקציה, על ידי בקשה ל"גישה אופליין", ואז להחליף את קוד האימות בטוקן רענון בשרת. חשוב מאוד לאחסן באופן מאובטח בשרת את טוקני הרענון לטווח ארוך, כי אפשר להשתמש בהם שוב ושוב כדי לקבל טוקני גישה חדשים. לכן, מטעמי אבטחה, מומלץ מאוד לא לאחסן טוקנים לרענון במכשיר. במקום זאת, צריך לאחסן אותם בשרתי הקצה העורפי של האפליקציה, שבהם מתבצעת ההחלפה באסימון גישה.

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

Kotlin

// Ask for offline access during the first authorization request
val authorizationRequest = AuthorizationRequest.builder()
    .setRequestedScopes(requestedScopes)
    .requestOfflineAccess(serverClientId)
    .build()

Identity.getAuthorizationClient(activity)
    .authorize(authorizationRequest)
    .addOnSuccessListener { authorizationResult ->
        startAuthorizationIntent.launch(IntentSenderRequest.Builder(
            pendingIntent!!.intentSender
        ).build())
    }
    .addOnFailureListener { e -> Log.e(TAG, "Failed to authorize", e) }

Java

// Ask for offline access during the first authorization request
AuthorizationRequest authorizationRequest = AuthorizationRequest.builder()
    .setRequestedScopes(requestedScopes)
    .requestOfflineAccess(serverClientId)
    .build();

Identity.getAuthorizationClient(getContext())
    .authorize(authorizationRequest)
    .addOnSuccessListener(authorizationResult -> {
        startAuthorizationIntent.launch(
            new IntentSenderRequest.Builder(
                authorizationResult.getPendingIntent().getIntentSender()
            ).build()
        );
    })
    .addOnFailureListener(e -> Log.e(TAG, "Failed to authorize"));

קטע הקוד הבא מניח שההרשאה מתחילה מקטע.

Kotlin

private lateinit var startAuthorizationIntent: ActivityResultLauncher<IntentSenderRequest>

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?,
): View? {
    // ...
    startAuthorizationIntent =
        registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { activityResult ->
            try {
                val authorizationResult = Identity.getAuthorizationClient(requireContext())
                    .getAuthorizationResultFromIntent(activityResult.data)
                // short-lived access token
                accessToken = authorizationResult.accessToken
                // store the authorization code used for getting a refresh token safely to your app's backend server
                val authCode: String = authorizationResult.serverAuthCode
                storeAuthCodeSafely(authCode)
            } catch (e: ApiException) {
                // log exception
            }
        }
}

Java

private ActivityResultLauncher<IntentSenderRequest> startAuthorizationIntent;

@Override
public View onCreateView(
    @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // ...
    startAuthorizationIntent =
        registerForActivityResult(
            new ActivityResultContracts.StartIntentSenderForResult(),
            activityResult -> {
                try {
                    AuthorizationResult authorizationResult =
                        Identity.getAuthorizationClient(requireActivity())
                            .getAuthorizationResultFromIntent(activityResult.getData());
                    // short-lived access token
                    accessToken = authorizationResult.getAccessToken();
                    // store the authorization code used for getting a refresh token safely to your app's backend server
                    String authCode = authorizationResult.getServerAuthCode()
                    storeAuthCodeSafely(authCode);
                } catch (ApiException e) {
                    // log exception
                }
            });
}