为了安全的访问在线服务,用户需要认证和授权他们的合法性。如果一个程序要访问第三方的服务,认证情况会更加复杂。不仅用户需要通过身份验证来使用服务,应用程序也需要通过认证才能代表用户的行为。
处理这种第三方认证的行业标准是 OAuth2 协议。OAuth2 提供了一个被称为 auth
token的值,来同时代表用户的身份和程序的授权。这节课程会演示如何通过OAuth2连接到Google的服务。虽然这里使用Google服务作为演示,对于其他支持OAuth2的服务也是同样适用的。
OAuth2 的优点:
- 从用户那里获取访问用户在线服务的许可
- 通过在线服务认证来代表用户行为
- 处理认证出错情况
收集信息
在开始使用 OAuth2 前,您需要知道关于该API的一些基础知识:
- 您需要访问的服务的URL地址
- auth scope定义了您程序申请的权限范围,访问Google Task的只读权限范围(Auth scope)是
View your tasks
,读写权限范围是Manage Your Tasks
。 - 一个 client id(客户端ID) 和 client secret(客户端密钥)是和服务器认证程序的字符串。您需要直接从所访问的服务器获取这两个字符串。Google有一个自有服务(self-service)系统来获取客户端id和密钥。这篇文章 在Android系统上使用 Tasks API 和 OAuth 2.0 介绍了如何使用该系统获取这两个字符串以及如何使用Google Tasks API。
申请一个 Auth Token
现在您将准备申请一个 auth token 了,这是一个需要多步骤的操作,如下图所示:
在获取一个 auth token 之前,您需要先在Manifest文件中申请 ACCOUNT_MANAGER
权限,同时由于大部分都是访问网络服务,所以当然还需要添加 INTERNET
权限。
1
2
3
4
5
|
<
manifest
... >
<
uses-permission
android:name
=
"android.permission.ACCOUNT_MANAGER"
/>
<
uses-permission
android:name
=
"android.permission.INTERNET"
/>
...
</
manifest
>
|
一旦您设置了这些权限,就可以通过 AccountManager.getAuthToken()
来得到一个 token 了。
当心!!调用 AccountManager
的方法是比较诡异的!由于账户认证操作可能需要访问网络,大部分的 AccountManager
函数都是同步的。这就意味着您需要实现一系列的回调函数接口才能完成认证过程,而不能只在一个函数中完成这项任务。示例代码如下:
1
2
3
4
5
6
7
8
9
10
|
AccountManager am = AccountManager.get(
this
);
Bundle options =
new
Bundle();
am.getAuthToken(
myAccount_,
// 通过getAccountsByType()函数获取Account
"Manage your tasks"
,
// Auth scope 授权范围
options,
// Authenticator-specific options
this
,
// 您的Activity
new
OnTokenAcquired(),
// 当成功获取到一个token时的回调函数
new
Handler(
new
OnError()));
// 发生错误时候的回调函数
|
上面的示例中, OnTokenAcquired
是一个继承至 AccountManagerCallback
的类。 AccountManager
类会调用OnTokenAcquired
类的 run()
函数并传递一个参数 AccountManagerFuture
,该参数包含了一个 Bundle
,如果成功的获取了token就可以从 Bundle
中获取了。
下面的代码演示了如何从 Bundle
中获取token:
1
2
3
4
5
6
7
8
9
10
11
12
|
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
中包含了一个合法的token,通过key KEY_AUTHTOKEN
就可以获取该token。有些情况下,并不总是会一帆风顺!
再次申请一个 Auth Token…
由于多种原因第一次获取一个token可能失败:
- 由于设备或者网络发生错误导致
AccountManager
失败了 - 用户决定不授权您的程序访问他们的账号
- 获取到的账号凭证权限比较低,不能完全访问用户账号
- 系统缓存的 auth token 过期了!
程序可以很轻松的处理前面两种情况,通常情况下只要显示一条错误提示信息即可。如果网络不可用或者用户不授权,您的程序也无能为力。而后面两种情况有点复杂,好的程序应该能自动的处理这两种情况。
第三种情况,也就是通过AccountManagerCallback
(上面示例代码中的OnTokenAcquired
) 获取到的 Bundle
中权限比较低。如果在 Bundle
中包含了一个 Intent
(查询KEY_INTENT
key),意味着认证器在给您一个有效的token之前需要先和用户交互。
有多种原因导致认证器返回一个 Intent
。可能由于用户第一次使用该设备;也有可能用户账户信息过期了, 需要用户再次登录;也需保存的用户凭证是错误的。还有可能系统需要多重认证或者需要启动相机来扫描视网膜以便确认用户的合法性。具体是啥原因对我们而言是不重要的。如果您需要一个有效的token,您就必需发送这个 Intent
。
1
2
3
4
5
6
7
8
9
10
11
|
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()
,这样您就可以在一个 Intent
中处理结果了(在您程序中Activity的onActivityResult()
函数中)。这一步非常重要!如果您没有处理返回的认证结果,就没法判定用户是否成功的授权了。如果结果是 RESULT_OK
表明授权成功,然后您就可以再次调用 AccountManager.getAuthToken()
来获取一个新的auth token了。
最后一种情况(Token过期了)其实并不是 AccountManager
的问题。只有连接到服务器才能知道这个Token是否过期,如果让 AccountManager
去不断的访问服务器来查询每个Token是否过期是非常浪费系统资源的。所以只有当您的程序使用该Token的时候才能发现她是否过期。
连接到在线服务
下面的示例中演示了如何连接到 Google 服务器。由于Google使用了标准的OAuth2协议,这里讨论的内容也完全适合其他使用OAuth2协议的服务。不过需要注意的是,每个服务都是不一样的。您可能需要做些稍微的调整来适用其他的服务。
对于每个请求, Google APIs 都需要四格参数:API
key、客户端ID、客户端密钥和auth key。前三个可以从 Google API Console 网站获取,而最后一个是通过调用 AccountManager.getAuthToken()
函数获取到的一个字符串。您通过HTTP请求把这些参数告诉Google服务器。
1
2
3
4
5
|
URL url =
new
URL(
"https://www.googleapis.com/tasks/v1/users/@me/lists?key="
+ <em>your_api_key</em>);
URLConnection conn = (HttpURLConnection) url.openConnection();
conn.addRequestProperty(
"client_id"
, <em>your client id</em>);
conn.addRequestProperty(
"client_secret"
, <em>your client secret</em>);
conn.setRequestProperty(
"Authorization"
,
"OAuth "
+ token);
|
如果请求返回HTTP 401错误,表明您的Token被拒绝了。就和上面描述的一样,大部分情况下都是因为您的Token过期了。只需要简单的调用 AccountManager.invalidateAuthToken()
函数删除缓存的Token并按照前面的方法再次获取一个Token即可。
由于token过期是比较常见的而且解决这个问题又是很简单的,所以大部分程序在获取Token之前都假设Token已经过期了。如果对于您的服务器签发一个新的Token也是一个很轻量的操作,您也可以选择在调用 AccountManager.getAuthToken()
函数之前先调用 AccountManager.invalidateAuthToken()
函数来删除之前的Token。