授予对 Google 用户数据的访问权限

身份验证用于确定用户的身份,通常称为用户注册或登录。授权是授予或拒绝访问数据或资源的过程。例如,您的应用请求用户同意访问用户的 Google 云端硬盘。

身份验证和授权调用应根据应用的需求分为两个单独且不同的流程。

如果您的应用具有可以利用 Google API 数据的功能,但这些功能并非应用核心功能的一部分,您应将应用设计为能够妥善处理无法访问 API 数据的情况。例如,当用户未授予云端硬盘访问权限时,您可能会隐藏最近保存的文件列表。

您应仅在用户执行需要访问特定 API 的操作时,才请求访问您需要访问 Google API 的作用域。例如,每当用户点按“保存到云端硬盘”按钮时,您都应请求访问用户云端硬盘的权限。

通过将授权与身份验证分开,您可以避免让新用户感到不知所措,或让用户对系统要求其提供某些权限的原因感到困惑。

对于身份验证,我们建议使用 Credential Manager API。对于需要访问 Google 存储的用户数据的授权操作,我们建议使用 AuthorizationClient

设置 项目

  1. 中打开您的项目,或者创建项目(如果您还没有项目)。
  2. 中,确保所有信息完整且准确无误。
    1. 确保您的应用已分配正确的应用名称、应用徽标和应用首页。这些值将在用户注册时显示在“使用 Google 账号登录”同意屏幕上,以及“第三方应用和服务”屏幕上。
    2. 请确保您已指定应用的隐私权政策和服务条款的网址。
  3. 中,为您的应用创建一个 Android 客户端 ID(如果您还没有)。您需要指定应用的软件包名称和 SHA-1 签名。
    1. 前往
    2. 点击创建客户端
    3. 选择 Android 应用类型。
  4. 中,创建一个新的“Web 应用”客户端 ID(如果您还没有)。您可以暂时忽略“已获授权的 JavaScript 来源”和“已获授权的重定向 URI”字段。此客户端 ID 将用于在您的后端服务器与 Google 的身份验证服务通信时标识该服务器。
    1. 前往
    2. 点击创建客户端
    3. 选择 Web 应用类型。

声明依赖项

在模块的 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.launchIntentSenderRequest.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 (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 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 格式。

从非默认账号进行授权

如果您使用凭据管理器进行身份验证,并运行 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.launchIntentSenderRequest.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"));

以下代码段假定授权是从 fragment 开始的。

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
                }
            });
}