为了安全地访问在线服务,用户需要向服务进行身份验证,即需要提供自己的身份证明。对于访问第三方服务的应用来说,安全问题更为复杂。用户不仅需要经过身份验证才能访问服务,而且还需要获得授权才能代表用户执行操作。
处理向第三方服务进行身份验证的业界标准方法是 OAuth2 协议。OAuth2 提供了单个值,称为身份验证令牌,它既表示用户的身份,也表示应用代表用户执行操作的授权。本课介绍如何连接到支持 OAuth2 的 Google 服务器。虽然以 Google 服务为例,但所演示的技术将适用于任何正确支持 OAuth2 协议的服务。
使用 OAuth2 有下列作用:
- 从用户处获取使用其帐号访问在线服务的权限。
- 代表用户对在线服务进行身份验证。
- 处理身份验证错误。
网罗信息,集思广益
要开始使用 OAuth2,您需要了解与所尝试访问的服务相关的一些 API 特定信息:
- 您要访问的服务的网址。
- 身份验证范围,这是一个字符串,用于定义应用要求的特定类型的访问。例如,对 Google Tasks 的只读权限的身份验证范围为
View your tasks
,而对 Google Tasks 的读写访问的身份验证范围为Manage your tasks
。 - 客户端 ID 和客户端密钥,它们是用于向服务标识应用的字符串。您需要直接从服务所有者处获取这些字符串。Google 提供了一个用于获取客户端 ID 和密钥的自助服务系统。
请求互联网权限
对于以 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
的原因或许多种多样。这可能是用户首次登录此帐号。可能是用户的帐号已过期,需要重新登录,也可能是存储的凭据不正确。或许该帐号需要进行双重身份验证,或者需要激活相机才能执行视网膜扫描。原因其实并不重要。如果您需要有效的令牌,则必须触发 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()
,这样就不必再请求两次身份验证令牌。