为了安全地访问在线服务,用户需要在服务上进行身份验证,即要提供他们的身份的证明。对于一个要访问第三方服务的程序来说,安全问题甚至更复杂。不仅仅是用户需要在访问服务前要进行身份验证,而且程序也要进行身份验证来授权用户。
OAuth2协议是一种向第三方服务进行身份验证的工业标准方法.OAuth2提供一个单值,叫做** 认证令牌(auth token)** ,代表用户身份和程序身份验证授权。这节课将要演示连接到一个支持OAuth2的Google服务器上。尽管Google服务只是用作示例,但是演示的这 项技术将会在任何正确支持OAuth2协议的服务上工作。
使用OAuth2有利于:
- 从用户手中得到授权访问那些需要他/她的账户的在线服务。
- 代替用户在一个在线服务上验证身份。
- 处理验证错误
收集信息
要开始使用Oauth2,你需要知道一些关于你要访问的API的信息:
- 你要访问的服务的地址。
- 认证范围(auth scope)。它是一个定义了你的应用需要的特定访问类型的字符串。例如,Google Tasks的只读访问认证范围范围是'''查看你的任务(View your tasks)''',而可读写访问的认证范围是'_管理你的任务(Manage Your Tasks)* 。
- 一个**客户端ID(client id)和客户端密钥(client secret)** 。他们是在服务中为了识别你的应用的字符串。你需要从服务提供者手中获取这些字符串。Google 有一个自服务系统用来获取客户端ID和密钥。Getting Started with the Tasks API and OAuth 2.0 on Android 这篇文章解释了如何使用这套系统来获取这些用于Google Tasks API的值对。
请求一个验证令牌
现在你已经准备好获取一个身份验证令牌了。获取步骤如下图所示。
为了得到一个验证令牌,首先需要在你的manifest文件中请求* ACCOUNT_MANAGER_'。要用这个令牌来真正做些事情,你也必须添加'_INTERNET* 权限。
- <manifest ... >
- <uses-permission android:name="android.permission.ACCOUNT_MANAGER" />
- <uses-permission android:name="android.permission.INTERNET" />
- ...
- </manifest>
一旦你的应用有了这些权限,你就可以调用AccountManager.getAuthToken() 方法来获取令牌。
注意了!在AccountManager_'中调用方法可能有些诡异!由于账户操作会涉及到网络通讯,因此大部分的AccountManager 的方法是异步的。这意味着相对于在一个方法中获取所有的令牌的工作,你需要把他们分散到一系列的回调函数中实现。例如:
- AccountManager am new Bundle();
- am.getAuthToken(
- myAccount_, // 用getAccountsByType()来检索的账户
- "Manage your tasks", // 令牌范围
- options, // 特殊验证选项
- this, // 你的activity
- new OnTokenAcquired(), // 成功获取令牌后调用的回调函数
- new Handler(new OnError())); // 错误发生时调用的回调函数
在这个例子中,OnTokenAcquired是一个继承了**AccountManagerCallback**的类。**AccountManager**在**OnTokenAcquired**中调用**run()**方法,该方法需要传递一个含有一个**Bundle**的**AccountManagerFuture**的实例。如果调用成功,那么这个令牌中就包含了这个Bundle。
这里展现如何从** Bundle** 中获取令牌的方法:
- 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.
- token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
- ...
- }
- }
如果这一切都运行顺利,那么这个** Bundle_'会在'_KEY_AUTHTOKEN** 中会包含一个有小额令牌,并且你就开始“启程远洋”了。尽管事情不会总是那么顺利……
请求一个验证令牌……再来一次
首先,你的一个验证令牌请求可能会因为一些原因而失败:
- 设备或网络错误导致
- 用户决定不想让你的应用访问他的账户。
- 保存的账户凭据不足以能够得到访问该账户的权限。
- 缓存的账户令牌已经过期。
应用程序可以用平常方式够处理前两种问题,通常做法是简单地把错误信息显示给用户。如果网络断开或者用户决定不同意访问,那么你的程序就没有太多可以做的事。最后两个问题有点复杂,因为运行正常的应用能够自行处理这些失败情形。
对于第三种失败情形,即没有充分的凭据,会通过你在* AccountManagerCallback_'(来自前一个例子的'''OnTokenAcqured''')中接收到的'''Bundle'''来沟通。如果这个'''Bundle'''在'''KEY_INTENT'''建中包含一个'_Intent* ,那么这个验证程序就会告诉你,在它可以给你一个可用的令牌之前,它需要和用户直接进行交互。
有很多原因可导致验证程序返回一个* Intent*。它可能是用户第一次登录账户的时候。或许该用户的账户已经过期却还要登陆,又或许他们存储的凭据本身就是错的。可能该账户需要双重认证或者它需要激活照相机来做虹膜扫描。具体什么原因并不重要,如果你想得到一个合法令牌,你就不得不一连串的询问* Intent* 来获得。
- private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
- @Override
- public void run(AccountManagerFuture<Bundle> result) {
- ...
- Intent launch = (Intent) result.get(AccountManager.KEY_INTENT);
- if (launch != null) {
- startActivityForResult(launch, 0);
- return;
- }
- }
- }
要注意这个例子中用了* startActivityForResult()) * 方法,所以你可以通过实现*onActivityResult())* 方法来捕获这个 * Intent* 的结果。这个非常重要!如果你不想从验证程序反馈的* Intent* 中捕获结果,那么你将不可能知道用户到底有没有成功验证。如果结果是* RESULT_OK*,那么验证程序已经更新并保存这些凭据,以便这些凭据足够达到你所请求的访问等级需求,然后你应该再次调用*AccountManager.getAuthToken()* 方法来请求新的认证令牌。
对于最后一个问题,即令牌已过期,这其实并不是一个* AccountManager_'的失败(failure)。唯一一种发现令牌是否过期的方法就是联系服务提供者,而且'_AccountManager* 不断地去线上检查所有令牌的状态时非常浪费并且代价昂贵的。所以这个失败只有当想你的一样的程序常使用认证令牌来访问线上服务时才能被检测到。
连接在线服务
下面的例子展示如何连接到Google服务器。由于Google 使用了工业标准OAuth2协议来认证请求,我们之前在这里讨论过的技术具有广泛的适用性。但是,请记住每个服务器是不同的。你可能自己会发现根据具体情况,这些访问账户的指令需要作出轻微的调整。
Google Api需要提供四个值以及与之对应的请求:API 键(key),客户端ID,客户端密钥,以及认证键(the auth key)。前三个来自Google API Console网站。最后一个是你通过调用* AccountManager.getAuthToken()* 获得的值。你把它们作为HTTP请求中的一部分传递到Google服务器。
- URL url " + 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())* 方法,并且把你的一次认证令牌请求拆成两次。