授權存取 Google 使用者資料

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

驗證和授權呼叫必須是兩個獨立且不同的流程,具體取決於應用程式的需求。

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

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

將授權與驗證分開,可避免新使用者不知所措,或不清楚為何需要授予特定權限。

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

設定 Google Cloud 控制台專案

  1. 在 Cloud 控制台中開啟專案,或建立新專案。
  2. 在「品牌宣傳」頁面,確認所有資訊完整正確。
    1. 確認應用程式已指派正確的應用程式名稱、應用程式標誌和應用程式首頁。使用者註冊時,會在「使用 Google 帳戶登入」同意畫面和第三方應用程式和服務畫面看到這些值。
    2. 請務必指定應用程式的隱私權政策和服務條款網址。
  3. 在「Clients」頁面中,為應用程式建立 Android 用戶端 ID (如果尚未建立)。您必須指定應用程式的套件名稱和 SHA-1 簽章。
    1. 前往「客戶」頁面。
    2. 按一下「Create client」(建立用戶端)。
    3. 選取「Android」應用程式類型。
    4. 輸入 OAuth 用戶端名稱。這個名稱會顯示在專案的「Clients」(用戶端) 頁面,用於識別用戶端。
    5. 輸入 Android 應用程式的套件名稱。這個值定義在 AndroidManifest.xml 檔案中 <manifest> 元素的 package 屬性 中。
    6. 輸入應用程式發布版本的 SHA-1 簽署憑證指紋。
    7. 如果應用程式使用 Google Play 應用程式簽署功能,請從 Play 管理中心的應用程式簽署頁面複製 SHA-1 指紋。
    8. 如果您自行管理 KeyStore 和簽署金鑰,請使用 Java 隨附的 keytool 公用程式,以人類可讀的格式列印憑證資訊。複製 keytool 輸出內容中 Certificate fingerprints 區段的 SHA-1 值。詳情請參閱 Google API for Android 說明文件中的「驗證用戶端」。
    9. (選用) 驗證 Android 應用程式的擁有權。
  4. 在「Clients」(用戶端) 頁面中,建立新的「網頁應用程式」用戶端 ID (如果尚未建立)。您可以暫時忽略「Authorized JavaScript Origins」和「Authorized redirect URIs」欄位。這個用戶端 ID 會用於識別後端伺服器,以便與 Google 的驗證服務通訊。
    1. 前往「客戶」頁面。
    2. 按一下「Create client」(建立用戶端)。
    3. 選取「網頁應用程式」類型。

驗證應用程式擁有權

您可以驗證應用程式擁有權,降低應用程式遭冒用的風險。

如要完成驗證程序,您可以使用 Google Play 開發人員帳戶 (如有),並在 Google Play 管理中心註冊應用程式。必須符合下列規定,才能順利完成驗證:

  • 您必須在 Google Play 管理中心註冊應用程式,且套件名稱和 SHA-1 簽署憑證指紋,必須與要完成驗證的 Android OAuth 用戶端相同。
  • 您必須在 Google Play 管理中心具備應用程式的管理員權限。 進一步瞭解 Google Play 管理中心的存取權管理。

在 Android 用戶端的「Verify App Ownership」部分,按一下「Verify Ownership」按鈕,完成驗證程序。

如果驗證成功,系統會顯示通知,確認驗證程序成功。否則系統會顯示錯誤提示。

如要修正驗證失敗的問題,請嘗試下列做法:

  • 確認要驗證的應用程式已在 Google Play 管理中心註冊。
  • 確認您在 Google Play 管理中心具備該應用程式的管理員權限。

宣告依附元件

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

dependencies {
  // ... other dependencies

  implementation "com.google.android.gms:play-services-auth:22.0.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 (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 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 帳戶,再要求授權。Credential Manager 的驗證回應包含使用者資訊 (例如電子郵件地址),並將應用程式的預設帳戶設為所選帳戶。如有需要,您可以在應用程式中追蹤這個帳戶。後續的授權要求會使用該帳戶做為預設帳戶,並略過授權流程中的帳戶選取步驟。如要使用其他帳戶進行授權,請參閱「使用非預設帳戶授權」。

  • 在授權要求中,除了您想要的範圍 (例如 Drive scope) 之外,請一併要求 userinfo、profile 和 openid 範圍。傳回存取權杖後,請使用偏好的 HTTP 程式庫,向 OAuth userinfo 端點 (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 格式呈現。

非預設帳戶授權

如果您使用 Credential Manager 進行驗證,並執行 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
                }
            });
}