18个月之前,我在开发Any.DD同步系统时,打算使用安卓提供的 但是,今非昔比… 因为一个最近着手的项目,我最近又开始研究相关功能,我突然发现这方面知识的丰富程度有了巨大的提升。包括Android.com上的更好的文档,外面的教程( 教程1 教程2 )也逐渐丰富了起来。让我们了解到声名狼藉的 但是,好像还是缺点什么。 我感觉整个流程并非尽知,有些部分并没有足够清楚。所以我决定用我的方法调查它——就像我平时想要了解一件事情所用的方法一样——用“杰克鲍尔“的方式。我发表了这篇深入调查后的结论性文章,包含了所有这个服务所能提供的功能和一些我觉得重要的需要发掘的细节。后面,我还会贴出一篇关于“SyncAdapter“的文章,所以如果读者感兴趣,我建议读者通过RSS或Twitter来订阅(我的博客)。我还是比较了解这方面的诸多细节,不仅仅是教程提供的简单功能。但如果我遗漏了什么,请在评论中指出。 为什么选择 Account Manager?为什么? 为什么不是写一个简单的登录表单,实现一个提交按钮,发送(post)所有信息到服务器,然后服务器返回一个鉴权令牌(auth token)?原因在于有很多(与用户鉴权相关的)附加功能和小细节你未必能考虑周全。这些容易被开发者忽略的小细节可能导致用户重新登录,或者被“100000个用户才会出现一次,无所谓“的声音 忽略掉。用户如果在另外一个客户端修改密码该如何处理?auth-token的过期判断呢?是要运行一个没有用户交互UI的后台服务吗?想要用户登录一次,相关APP就可以自动登录的便利吗?(就像Google的APP那样) 读这篇文章之前或许让你感觉有些东西太复杂,但其实不是。对于绝大多数应用场景来说, Account Manager都简化了登录过程。而且我也给你提供了代码样例,还有什么理由不用呢? 好吧,让我们来看看(使用 好处:标准的用户鉴权方式;为开发者简化了登录的流程;处理访问拒绝的场景;可以为一个账户处理不同类型的访问令牌(如:只读、全权限);轻松的在不同程序间共享令牌;有如Sync Adapter这样的后台处理的良好支持;并且,在手机的Setting界面中有一个很酷的入口:
看,妈妈,设置屏幕上有我的”名纸”! 缺陷:需要学习它!但是,嗨,这不就是你读此文的目的吗? 要实现这些功能,需要下面几步: 1. 创建 2. 创建若干 3. 创建 首先,(来看)一些概念。 Authenti..啥?授权令牌 (Authentication Token, auth-token ) – 是由服务器提供的一个临时的访问令牌。所有需要识别用户的请求,在发送到服务器时都要带着这个令牌。在这篇文章中,我们使用 OAuth2 ,它也是现在最为流行的方法。 授权服务器– 用来管理所有用户的服务器。它将会为登录到服务器的用户生成授权令牌(auth-token),并且校验所有的用户请求(是否合法)。授权令牌有时间限制,过期后将时效。 AccountManager– 管理设备上的所有账户,也是这项功能的核心。App从 AccountAuthenticator– 是一个为具体账户类型提供鉴权处理过程的组件。 AccountAuthenticatorActivity– “登录/创建用户“ 当你的App需要auth-token时,只需调用 这里获得更多如何使用OAuth2进行认证的文章。 现在,我们已经了解了基础知识。现在来看看如何建立一个自有账户类型的authenticator。 建立我们自己的Authenticator 如前文所述, Account Authenticator 由 建立我们自己的 addAccount当用户打算登录并在一个设备上新建账户时,会调用这个方法。 我们需要返回一个Bundle,其中包含一个会启动我们自己的_
例如: @Override public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException { final Intent intent = new Intent(mContext, AuthenticatorActivity.class); intent.putExtra(AuthenticatorActivity.ARG_ACCOUNT_TYPE, accountType); intent.putExtra(AuthenticatorActivity.ARG_AUTH_TYPE, authTokenType); intent.putExtra(AuthenticatorActivity.ARG_IS_ADDING_NEW_ACCOUNT, true); intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); final Bundle bundle = new Bundle(); bundle.putParcelable(AccountManager.KEY_INTENT, intent); return bundle; } @Override publicBundleaddAccount(AccountAuthenticatorResponseresponse,StringaccountType,StringauthTokenType,String[]requiredFeatures,Bundleoptions)throwsNetworkErrorException{ finalIntentintent=newIntent(mContext,AuthenticatorActivity.class); intent.putExtra(AuthenticatorActivity.ARG_ACCOUNT_TYPE,accountType); intent.putExtra(AuthenticatorActivity.ARG_AUTH_TYPE,authTokenType); intent.putExtra(AuthenticatorActivity.ARG_IS_ADDING_NEW_ACCOUNT,true); intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,response); finalBundlebundle=newBundle(); bundle.putParcelable(AccountManager.KEY_INTENT,intent); returnbundle; } getAuthToken 如上面的流程图所示,getAuthToken可以获取存储在设备上的已经登陆成功用户的auth-token。如果auth-token不存在,将会提示用户登录。在成功登陆之后,请求auth-token的app会“长等待“此token。为了避免此情况,我们应该通过 <br />@Override public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { // Extract the username and password from the Account Manager, and ask // the server for an appropriate AuthToken. final AccountManager am = AccountManager.get(mContext); String authToken = am.peekAuthToken(account, authTokenType); // Lets give another try to authenticate the user if (TextUtils.isEmpty(authToken)) { final String password = am.getPassword(account); if (password != null) { authToken = sServerAuthenticate.userSignIn(account.name, password, authTokenType); } } // If we get an authToken - we return it if (!TextUtils.isEmpty(authToken)) { final Bundle result = new Bundle(); result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); result.putString(AccountManager.KEY_AUTHTOKEN, authToken); return result; } // If we get here, then we couldn't access the user's password - so we // need to re-prompt them for their credentials. We do that by creating // an intent to display our AuthenticatorActivity. final Intent intent = new Intent(mContext, AuthenticatorActivity.class); intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); intent.putExtra(AuthenticatorActivity.ARG_ACCOUNT_TYPE, account.type); intent.putExtra(AuthenticatorActivity.ARG_AUTH_TYPE, authTokenType); final Bundle bundle = new Bundle(); bundle.putParcelable(AccountManager.KEY_INTENT, intent); return bundle; } <br/>@Override publicBundlegetAuthToken(AccountAuthenticatorResponseresponse,Accountaccount,StringauthTokenType,Bundleoptions)throwsNetworkErrorException{ // Extract the username and password from the Account Manager, and ask // the server for an appropriate AuthToken. finalAccountManageram=AccountManager.get(mContext); StringauthToken=am.peekAuthToken(account,authTokenType); // Lets give another try to authenticate the user if(TextUtils.isEmpty(authToken)){ finalStringpassword=am.getPassword(account); if(password!=null){ authToken=sServerAuthenticate.userSignIn(account.name,password,authTokenType); } } // If we get an authToken - we return it if(!TextUtils.isEmpty(authToken)){ finalBundleresult=newBundle(); result.putString(AccountManager.KEY_ACCOUNT_NAME,account.name); result.putString(AccountManager.KEY_ACCOUNT_TYPE,account.type); result.putString(AccountManager.KEY_AUTHTOKEN,authToken); returnresult; } // If we get here, then we couldn't access the user's password - so we // need to re-prompt them for their credentials. We do that by creating // an intent to display our AuthenticatorActivity. finalIntentintent=newIntent(mContext,AuthenticatorActivity.class); intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,response); intent.putExtra(AuthenticatorActivity.ARG_ACCOUNT_TYPE,account.type); intent.putExtra(AuthenticatorActivity.ARG_AUTH_TYPE,authTokenType); finalBundlebundle=newBundle(); bundle.putParcelable(AccountManager.KEY_INTENT,intent); returnbundle; } 如果我们通过此方法获得的auth-token已经无效了,比如过期了或者用户从其他客户端修改了密码。我们应该调用 所以,用户要在哪输入验证信息?这就是 创建Activity AccountAuthenticatorActivity 是整个过程中唯一直接与用户交互的 我在我的 public void submit() { final String userName = ((TextView) findViewById(R.id.accountName)).getText().toString(); final String userPass = ((TextView) findViewById(R.id.accountPassword)).getText().toString(); new AsyncTask<Void, Void, Intent>() { @Override protected Intent doInBackground(Void... params) { String authtoken = sServerAuthenticate.userSignIn(userName, userPass, mAuthTokenType); final Intent res = new Intent(); res.putExtra(AccountManager.KEY_ACCOUNT_NAME, userName); res.putExtra(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE); res.putExtra(AccountManager.KEY_AUTHTOKEN, authtoken); res.putExtra(PARAM_USER_PASS, userPass); return res; } @Override protected void onPostExecute(Intent intent) { finishLogin(intent); } }.execute(); } publicvoidsubmit(){ finalStringuserName=((TextView)findViewById(R.id.accountName)).getText().toString(); finalStringuserPass=((TextView)findViewById(R.id.accountPassword)).getText().toString(); newAsyncTask<Void,Void,Intent>(){ @Override protectedIntentdoInBackground(Void...params){ Stringauthtoken=sServerAuthenticate.userSignIn(userName,userPass,mAuthTokenType); finalIntentres=newIntent(); res.putExtra(AccountManager.KEY_ACCOUNT_NAME,userName); res.putExtra(AccountManager.KEY_ACCOUNT_TYPE,ACCOUNT_TYPE); res.putExtra(AccountManager.KEY_AUTHTOKEN,authtoken); res.putExtra(PARAM_USER_PASS,userPass); returnres; } @Override protectedvoidonPostExecute(Intentintent){ finishLogin(intent); } }.execute(); } 完成后,调用 finishLogin(): private void finishLogin(Intent intent) { String accountName = intent.getStringExtra(AccountManager.KEY_ACCOUNT_NAME); String accountPassword = intent.getStringExtra(PARAM_USER_PASS); final Account account = new Account(accountName, intent.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE)); if (getIntent().getBooleanExtra(ARG_IS_ADDING_NEW_ACCOUNT, false)) { String authtoken = intent.getStringExtra(AccountManager.KEY_AUTHTOKEN); String authtokenType = mAuthTokenType; // Creating the account on the device and setting the auth token we got // (Not setting the auth token will cause another call to the server to authenticate the user) mAccountManager.addAccountExplicitly(account, accountPassword, null); mAccountManager.setAuthToken(account, authtokenType, authtoken); } else { mAccountManager.setPassword(account, accountPassword); } setAccountAuthenticatorResult(intent.getExtras()); setResult(RESULT_OK, intent); finish(); } privatevoidfinishLogin(Intentintent){ StringaccountName=intent.getStringExtra(AccountManager.KEY_ACCOUNT_NAME); StringaccountPassword=intent.getStringExtra(PARAM_USER_PASS); finalAccountaccount=newAccount(accountName,intent.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE)); if(getIntent().getBooleanExtra(ARG_IS_ADDING_NEW_ACCOUNT,false)){ Stringauthtoken=intent.getStringExtra(AccountManager.KEY_AUTHTOKEN); StringauthtokenType=mAuthTokenType; // Creating the account on the device and setting the auth token we got // (Not setting the auth token will cause another call to the server to authenticate the user) mAccountManager.addAccountExplicitly(account,accountPassword,null); mAccountManager.setAuthToken(account,authtokenType,authtoken); }else{ mAccountManager.setPassword(account,accountPassword); } setAccountAuthenticatorResult(intent.getExtras()); setResult(RESULT_OK,intent); finish(); } 通过上面的方法我们获得了一个全新的auth-token,具体细节如下:
注意:addAccountExplicitly() 的第三个参数,是用户数据Bundle,它可以用在 在Activity完成登陆之后,我们已经为 现在一切流程准备就绪,那谁来启动这个过程呢?(其他应用)如何来访问它?我们需要让我们的 创建ServiceService非常简单。 我们要做的,是让其他的进程与我们的服务绑定,并于 现在我们的服务是这样的: public class UdinicAuthenticatorService extends Service { @Override public IBinder onBind(Intent intent) { UdinicAuthenticator authenticator = new UdinicAuthenticator(this); return authenticator.getIBinder(); } } publicclassUdinicAuthenticatorServiceextendsService{ @Override publicIBinderonBind(Intentintent){ UdinicAuthenticatorauthenticator=newUdinicAuthenticator(this); returnauthenticator.getIBinder(); } } ..and on the manifest we need to add our service with the 在manifest文件中,需要对Service声明 <service android:name=".authentication.UdinicAuthenticatorService"> <intent-filter> <action android:name="android.accounts.AccountAuthenticator" /> </intent-filter> <meta-data android:name="android.accounts.AccountAuthenticator" android:resource="@xml/authenticator" /> </service> <serviceandroid:name=".authentication.UdinicAuthenticatorService"> <intent-filter> <actionandroid:name="android.accounts.AccountAuthenticator"/> </intent-filter> <meta-dataandroid:name="android.accounts.AccountAuthenticator" android:resource="@xml/authenticator"/> </service> 很简单,是吧? 作为资源引用的 <account-authenticator xmlns:android="http://schemas./apk/res/android" android:accountType="com.udinic.auth_example" android:icon="@drawable/ic_udinic" android:smallIcon="@drawable/ic_udinic" android:label="@string/label" android:accountPreferences="@xml/prefs"/> <account-authenticatorxmlns:android="http://schemas./apk/res/android" android:accountType="com.udinic.auth_example" android:icon="@drawable/ic_udinic" android:smallIcon="@drawable/ic_udinic" android:label="@string/label" android:accountPreferences="@xml/prefs"/> 让我们来解释一下:
你需要了解的其他特性在我调查的过程中,我发现了一些有意思的场景。为了让你使用相关API时不致想破头,我把它分享出来。
“为一个设备上不存在的账户请求auth-token,将会导致未定义的失败。“ 在我这,这个“未定义的失败“虽热调用了登录画面,但当我输入认证信息后却没有任何反应,所以你得注意一下。
“Full access to..”字符串是通过我们的 1, 存储密码 – 接下来?现在,你已经对这个很棒的服务比较熟悉了。你可以在Google Play上 下载我开发的样例程序 。它会在你的设备创建“Udinic account”类型的账户,验证则会通过Parse.com账户来进行。样例应用提供了下面几项功能:
getAuthToken按钮首先会查询设备上是否有“Udinic”类型的账户。如果有,它通过调用 getAuthTokenByFeatures调用了
如果你打算让token失效,你可以用 你可以在 Github 上下载相关源代码。里面包含了2个样例应用,所以你可以尝试下2个应用之间共享相同的 |
|