授權存取 Google 使用者資料

驗證是確認使用者身分的程序,通常稱為使用者註冊或登入。授權是授予或拒絕存取資料或資源的程序。舉例來說,您的應用程式會要求使用者同意存取 Google 雲端硬碟。

驗證和授權呼叫應根據應用程式需求,分別採用兩種不同的流程。

如果應用程式有可使用 Google API 資料的功能,但這些功能並非應用程式核心功能的一部分,您應設計應用程式,以便妥善處理無法存取 API 資料的情況。舉例來說,如果使用者尚未授予雲端硬碟存取權,您可能會隱藏最近儲存的檔案清單。

只有在使用者執行需要存取特定 Google API 的動作時,您才應要求存取所需範圍。舉例來說,每當使用者輕觸「儲存到雲端硬碟」按鈕時,您都應要求存取使用者雲端硬碟的權限。

將授權與驗證分開,可避免新使用者感到困惑,或不明白為何系統要求他們提供特定權限。

建議使用 Credential Manager API 進行驗證。如要授權需要存取 Google 儲存使用者資料的動作,建議使用 AuthorizationClient

設定專案 Google Cloud Console

  1. Cloud Console 中開啟專案,或建立專案 (如果尚未建立)。
  2. 在「Branding page」中,確認所有資訊完整且正確。
    1. 請確認應用程式已指派正確的應用程式名稱、應用程式標誌和應用程式首頁。系統會在註冊時的「使用 Google 帳戶登入」同意畫面,以及「第三方應用程式和服務」畫面中,向使用者顯示這些值。
    2. 請確認您已指定應用程式隱私權政策和服務條款的網址。
  3. Clients page 中,為應用程式建立 Android 用戶端 ID (如果還沒有的話)。您需要指定應用程式的套件名稱和 SHA-1 簽章。
    1. 前往 Clients page
    2. 按一下「建立用戶端」
    3. 選取「Android」應用程式類型。
  4. Clients page 中,建立新的「網頁應用程式」用戶端 ID (如果尚未建立)。目前可以忽略「已授權的 JavaScript 來源」和「已授權的重新導向 URI」欄位。後端伺服器與 Google 驗證服務通訊時,會使用這個用戶端 ID 識別自己。
    1. 前往 Clients page
    2. 按一下「建立用戶端」
    3. 選取「網頁應用程式」類型。

宣告依附元件

在模組的 build.gradle 檔案中,使用最新版本的 Google Identity Services 程式庫宣告依附元件。

dependencies {
  // ... other dependencies

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

要求使用者動作所需的權限

每當使用者執行需要額外範圍的動作時,請呼叫 AuthorizationClient.authorize()。舉例來說,如果使用者執行的動作需要存取雲端硬碟應用程式儲存空間,請按照下列步驟操作:

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 時,請按照下方程式碼片段所示處理回應,其中假設是在片段中完成。程式碼會檢查是否已成功授予必要權限,然後執行使用者動作。

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 (ApiException e) {
                // 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 API,請從 AuthorizationResult 呼叫 getServerAuthCode() 方法,取得授權碼並傳送至後端,以交換存取權和更新權杖。詳情請參閱「持續存取使用者的資料」。

撤銷使用者資料或資源的權限

如要撤銷先前授予的存取權,請呼叫 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 雲端硬碟的回應,不會揭露使用者選取的帳戶身分,但可用於存取使用者雲端硬碟中的檔案。如要取得使用者的姓名或電子郵件地址等資訊,請使用下列選項:

  • 請先使用 Credential Manager API,讓使用者登入 Google 帳戶,再要求授權。憑證管理工具的驗證回應會包含電子郵件地址等使用者資訊,並將應用程式的預設帳戶設為所選帳戶;如有需要,您可以在應用程式中追蹤這個帳戶。後續的授權要求會使用這個帳戶做為預設帳戶,並略過授權流程中的帳戶選取步驟。如要使用其他帳戶授權,請參閱「從非預設帳戶授權」。

  • 在授權要求中,除了您想要的範圍 (例如 Drive scope) 之外,也請要求 userinfoprofileopenid 範圍。存取權杖傳回後,請使用偏好的 HTTP 程式庫,對 OAuth 使用者資訊端點 (https://www.googleapis.com/oauth2/v3/userinfo) 發出 GET HTTP 要求,並在標頭中加入您收到的存取權杖,即可取得使用者資訊,相當於下列 curl 指令:

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

    回覆是 UserInfo,僅限於所要求的範圍,並以 JSON 格式提供。

從非預設帳戶授權

如果您使用認證管理工具進行驗證並執行 AuthorizationClient.authorize(),應用程式的預設帳戶會設為使用者選取的帳戶。也就是說,後續的授權呼叫都會使用這個預設帳戶。如要強制顯示帳戶選取器,請使用 Credential Manager 的 clearCredentialState() API 將使用者登出應用程式。

持續存取使用者的資料

如要從應用程式存取使用者資料,請呼叫 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
                }
            });
}