对 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 和密钥的自助服务系统。

请求互联网权限

对于以 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 使用包含 BundleAccountManagerFutureOnTokenAcquired 调用 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);
        ...
    }
}

如果一切顺利,BundleKEY_AUTHTOKEN 键中会包含一个有效令牌,而您已大功告成。

对身份验证令牌的第一个请求可能会失败,原因如下:

  • 设备或网络出错,导致 AccountManager 出现故障。
  • 用户决定不向您的应用授予访问该账号的权限。
  • 存储的账号凭据不足以获取对该账号的访问权限。
  • 缓存的身份验证令牌已过期。

应用通常只需向用户显示错误消息即可轻松处理前两种情况。如果网络中断或用户决定不授予访问权限,那么您的应用对此将无能为力。最后两种情况稍微复杂一些,因为运行状况良好的应用理应自动处理这些故障。

第三种失败情况(凭据不足)将通过您在 AccountManagerCallback(上一示例中的 OnTokenAcquired)中收到的 Bundle 进行传达。如果 BundleKEY_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(),这样就不必再请求两次身份验证令牌。