身份验证用于确定某人的身份,通常称为用户注册或登录。授权是授予或拒绝访问数据或资源的过程。例如,您的应用请求用户同意访问用户的 Google 云端硬盘。
身份验证和授权调用必须是两个单独且不同的流程,具体取决于应用的需求。
如果您的应用具有可以利用 Google API 数据的功能,但这些功能并非应用核心功能的一部分,请将应用设计为能够妥善处理无法访问 API 数据的情况。例如,当用户未授予云端硬盘访问权限时,您可以隐藏最近保存的文件列表。
只有当用户执行需要访问特定 API 的操作时,您才应请求访问需要访问 Google API 的范围。例如,每当用户点按保存到云端硬盘 按钮时,您都应请求访问用户云端硬盘的权限。
通过将授权与身份验证分开,您可以避免让新用户感到不知所措,或让用户对为何被要求授予某些权限感到困惑。
对于身份验证,我们建议使用 Credential Manager API。对于 需要访问 Google 存储的用户数据的授权操作,我们建议 使用 AuthorizationClient。
设置 Google Cloud 控制台项目
- 在 Cloud 控制台中打开您的项目 ,或者创建一个项目(如果您还没有项目)。
- 在“品牌”页面上,
确保所有信息完整且准确。
- 确保您的应用已分配正确的应用名称、应用徽标和应用首页。这些值将在注册时的“使用 Google 账号登录”权限请求页面和“第三方应用和服务”屏幕上向用户显示。
- 确保您已指定应用的隐私权政策和服务条款的网址。
- 在“客户端”页面中,
为您的应用创建一个 Android 客户端 ID(如果您还没有)。您需要指定应用的软件包名称和 SHA-1 签名。
- 前往“客户端”页面。
- 点击创建客户端 。
- 选择 Android 应用类型。
- 输入 OAuth 客户端的名称。此名称会显示在项目的 “客户端”页面上,用于标识客户端。
- 输入 Android 应用的软件包名称。此值在
package属性的<manifest>元素 在您的AndroidManifest.xml文件中定义。 - 输入应用分发的 SHA-1 签名证书指纹。
- 如果您的应用使用 Google Play 应用签名, 请从 Play 管理中心的“应用签名”页面复制 SHA-1 指纹。
- 如果您自行管理密钥库和签名密钥,请使用 keytool
实用程序(Java 附带)以
人类可读的格式输出证书信息。复制
SHA-1值在Certificate fingerprints部分的 keytool 输出。 如需了解详情,请参阅 Google APIs for Android 文档中的对客户端进行身份验证。 - (可选)验证所有权的 Android 应用。
- 在“客户端”页面中,
创建一个新的“Web 应用”客户端 ID(如果您还没有)。您现在可以忽略“已获授权的 JavaScript 来源”和“已获授权的重定向 URI”字段。此客户端 ID 将用于在后端服务器与 Google 的身份验证服务通信时标识后端服务器。
- 前往“客户端”页面。
- 点击创建客户端 。
- 选择Web 应用 类型。
验证应用所有权
您可以验证应用的所有权,以降低应用被冒充的风险。
如需完成验证流程,您可以使用 Google Play 开发者账号(如果您有)以及在 Google Play 管理中心内注册的应用 。如需成功完成验证,您必须满足以下要求:
- 您必须在 Google Play 管理中心内注册应用,且该应用的软件包名称和 SHA-1 签名证书指纹与您要完成验证的 Android OAuth 客户端相同。
- 您必须在 Google Play 管理中心内拥有该应用的管理员 权限。 详细了解 Google Play 管理中心内的访问权限管理。
在 Android 客户端的验证应用所有权 部分中,点击验证所有权 按钮以完成验证流程。
如果验证成功,系统会显示一条通知,确认验证流程成功完成。否则,系统会显示错误提示。
如需修复验证失败问题,请尝试以下操作:
- 确保您要验证的应用是在 Google Play 管理中心内注册的应用。
- 确保您在 Google Play 管理中心内拥有该应用的管理员 权限。
声明依赖项
在模块的 build.gradle 文件中,使用最新版本 Google Identity 服务 库声明依赖项。
dependencies {
// ... other dependencies
implementation "com.google.android.gms:play-services-auth:21.5.1"
}
请求用户操作所需的权限
每当用户执行需要额外范围的操作时,请调用 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 时,请按照以下代码段所示处理响应,其中我们假定是在 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 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) 发出GETHTTP 请求,并在标头中添加您收到的访问令牌,即可获取用户信息,这相当于以下curl命令:curl -X GET \ "https://www.googleapis.com/oauth2/v1/userinfo?alt=json" \ -H "Authorization: Bearer $TOKEN"响应是
UserInfo,仅限于所请求的范围,采用 JSON 格式。
使用非默认账号进行授权
如果您使用 Credential Manager 进行身份验证并运行
AuthorizationClient.authorize(),则应用默认账号会设置为
用户选择的账号。这意味着,任何后续授权调用都会使用此默认账号。如需强制显示账号选择器,
请使用 clearCredentialState() API 从
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"));
以下代码段假定授权是从 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
}
});
}