Android NDK开发详解用户数据和身份之对 OAuth2 服务进行身份验证身份验证令牌逻辑图
图 1. 从 Android 帐号管理器获取有效身份验证令牌的流程
为了安全地访问在线服务,用户需要对服务进行身份验证,即,他们需要提供其身份证明。对于访问第三方服务的应用来说,安全问题更为复杂。用户需要通过身份验证才能访问服务,同时,应用需要相应授权才能代表用户执行操作。
OAuth2 协议是处理第三方服务身份验证的业界标准方法。OAuth2 可提供称为身份验证令牌的单个值,它既代表用户的身份,又包含应用代表用户执行操作所需的授权。本课程介绍了如何连接到支持 OAuth2 的 Google 服务器。尽管本课程以 Google 服务为例,但其中介绍的技巧适用于能够正确支持 OAuth2 协议的所有服务。
使用 OAuth2 有下列作用:
从用户处获取使用其帐号访问在线服务的许可。
代表用户对在线服务进行身份验证。
处理身份验证错误。
收集信息
如需开始使用 OAuth2,您需要了解有关待访问 API 的一些信息:
待访问服务的网址。
身份验证范围,这是一个字符串,定义了应用要求的特定类型的访问权限。例如,对 Google Tasks 的只读权限的身份验证范围为 View your tasks,而对 Google Tasks 的读写权限的身份验证范围为 Manage your tasks。
客户端 ID 和客户端密钥,它们是用于向服务表明应用身份的字符串。您需要直接从服务所有者那里获取这些字符串。Google 提供了一个自助服务系统,用于获取客户端 ID 和客户端密钥。授权和使用 REST API 一文介绍了如何使用此系统来获取这些值,以便与 Google Tasks API 结合使用。
请求互联网权限
对于以 Android 6.0(API 级别 23)及更高版本为目标平台的应用,getAuthToken() 方法本身不需要任何权限。不过,如果要对令牌执行操作,您需要将 INTERNET 权限添加到清单文件中,如以下代码段所示:
<manifest ... >
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>
请求身份验证令牌
如需获取令牌,请调用 AccountManager.getAuthToken()。
注意:由于某些帐号操作可能涉及网络通信,因此大多数 AccountManager 方法都是异步的。这意味着,您无需在一个函数中完成所有身份验证工作,而是需要将其实现为一系列回调。
以下代码段展示了如何使用一系列回调来获取令牌:
Kotlin
val am: AccountManager = AccountManager.get(this)
val options = Bundle()
am.getAuthToken(
myAccount_, // Account retrieved using getAccountsByType()
"Manage your tasks", // Auth scope
options, // Authenticator-specific options
this, // Your activity
OnTokenAcquired(), // Callback called when a token is successfully acquired
Handler(OnError()) // Callback called if an error occurs
)
Java
AccountManager am = AccountManager.get(this);
Bundle options = new Bundle();
am.getAuthToken(
myAccount_, // Account retrieved using getAccountsByType()
"Manage your tasks", // Auth scope
options, // Authenticator-specific options
this, // Your activity
new OnTokenAcquired(), // Callback called when a token is successfully acquired
new Handler(new OnError())); // Callback called if an error occurs
在此示例中,OnTokenAcquired 是实现 AccountManagerCallback 的类。AccountManager 使用包含 Bundle 的 AccountManagerFuture 对 OnTokenAcquired 调用 run()。如果调用成功,则令牌位于 Bundle 之内。
以下是从 Bundle 获取令牌的方法:
Kotlin
private class OnTokenAcquired : AccountManagerCallback<Bundle> {
override fun run(result: AccountManagerFuture<Bundle>) {
// Get the result of the operation from the AccountManagerFuture.
val bundle: Bundle = result.getResult()
// The token is a named value in the bundle. The name of the value
// is stored in the constant AccountManager.KEY_AUTHTOKEN.
val token: String = bundle.getString(AccountManager.KEY_AUTHTOKEN)
}
}
Java
private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
@Override
public void run(AccountManagerFuture<Bundle> result) {
// Get the result of the operation from the AccountManagerFuture.
Bundle bundle = result.getResult();
// The token is a named value in the bundle. The name of the value
// is stored in the constant AccountManager.KEY_AUTHTOKEN.
String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
...
}
}
如果一切进展顺利,Bundle 的 KEY_AUTHTOKEN 密钥中会包含一个有效令牌,而您可以继续操作。不过,事情不一定总会那么顺利…
再次请求身份验证令牌
您第一次请求身份验证令牌的操作可能因如下原因而失败:
设备或网络出错,导致 AccountManager 出现故障。
用户决定不向您的应用授予访问该帐号的权限。
存储的帐号凭据不足以获取对该帐号的访问权限。
缓存的身份验证令牌已过期。
应用通常只需向用户显示错误消息即可轻松处理前两种情况。如果网络中断或用户决定不授予访问权限,那么应用对此将无能为力。最后两种情况稍微复杂一些,因为运行状况良好的应用理应能自动处理这些故障。
第三种失败情况(凭据不足)通过您在 AccountManagerCallback(之前一个示例中的 OnTokenAcquired)中接收的 Bundle 进行传达。如果 Bundle 的 KEY_INTENT 密钥中包含 Intent,那么身份验证器会告知您它需要直接与用户互动,然后才能为您提供有效的令牌。
身份验证器返回 Intent 的原因或许多种多样。可能是因为这是用户首次登录此帐号。可能用户的帐号已过期,需要重新登录;或者,他们存储的凭据可能不正确。也许该帐号需要进行双重身份验证,或者需要激活相机才能执行 Retina 扫描。原因是哪个其实并不重要。如果您想要获取有效令牌,那么您必须触发 Intent 才能获取该令牌。
Kotlin
private inner class OnTokenAcquired : AccountManagerCallback<Bundle> {
override fun run(result: AccountManagerFuture<Bundle>) {
val launch: Intent? = result.getResult().get(AccountManager.KEY_INTENT) as? Intent
if (launch != null) {
startActivityForResult(launch, 0)
}
}
}
Java
private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
@Override
public void run(AccountManagerFuture<Bundle> result) {
...
Intent launch = (Intent) result.getResult().get(AccountManager.KEY_INTENT);
if (launch != null) {
startActivityForResult(launch, 0);
return;
}
}
}
请注意,该示例使用了 startActivityForResult(),因此您可以通过在自己的 Activity 中实现 onActivityResult() 来获取 Intent 的结果。这一点很重要!如果您没有从身份验证程序的响应 Intent 中获取结果,则无法确定用户是否已成功进行身份验证。如果结果为 RESULT_OK,则表示身份验证器已更新存储的凭据,因此能够达到您所请求的访问权限级别,而您应该再次调用 AccountManager.getAuthToken(),以请求新的身份验证令牌。
最后一种情况为令牌已过期,它实际上不属于 AccountManager 出现故障的情况。要了解某个令牌是否已过期,唯一的办法就是与服务器联系;让 AccountManager 不断联网检查所有令牌状态的做法既浪费资源,代价又高昂。因此,只有当应用尝试使用该身份验证令牌访问在线服务时,才会检测到这种失败情况。
连接到在线服务
以下示例展示了如何连接到 Google 服务器。由于 Google 使用业界标准的 OAuth2 协议对请求进行身份验证,因此这里讨论的技巧广泛适用于各种情况。但请注意,服务器各有不同。您可能会发现自己需要根据具体情况对这些说明进行细微的调整。
Google API 要求您为每个请求提供四个值:API 密钥、客户端 ID、客户端密钥和身份验证密钥。前三个来自 Google API 控制台网站。最后一个是通过调用 AccountManager.getAuthToken() 获取的字符串值。您可以在 HTTP 请求中将这些值传递给 Google 服务器。
Kotlin
val url = URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=$your_api_key")
val conn = url.openConnection() as HttpURLConnection
conn.apply {
addRequestProperty("client_id", your client id)
addRequestProperty("client_secret", your client secret)
setRequestProperty("Authorization", "OAuth $token")
}
Java
URL url = new URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=" + your_api_key);
URLConnection conn = (HttpURLConnection) url.openConnection();
conn.addRequestProperty("client_id", your client id);
conn.addRequestProperty("client_secret", your client secret);
conn.setRequestProperty("Authorization", "OAuth " + token);
如果请求返回 HTTP 错误代码 401,则表示您的令牌已被拒绝。正如上一部分中所述,出现这种情况最常见的原因是令牌已过期。解决方法很简单:调用 AccountManager.invalidateAuthToken() 并再次执行令牌获取流程。
令牌过期的情况很常见,并且修复它们也非常简单,因此,许多应用甚至在请求令牌之前便会假定令牌已过期。如果续订令牌对您的服务器而言耗用资源较少,您不妨在首次调用 AccountManager.getAuthToken() 之前先调用 AccountManager.invalidateAuthToken(),这样便无需请求两次身份验证令牌。
本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。
最后更新时间 (UTC):2020-06-05。