Google 致力于为黑人社区推动种族平等。查看具体举措

对 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 使用包含 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 的原因或许多种多样。可能是因为这是用户首次登录此帐号。可能用户的帐号已过期,需要重新登录;或者,他们存储的凭据可能不正确。也许该帐号需要进行双重身份验证,或者需要激活相机才能执行 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(),这样便无需请求两次身份验证令牌。