Android + Django + OAuth2 + Stub Authenticator

9 篇文章 0 订阅
5 篇文章 0 订阅

概况

最近研究了一下如何在Android上添加新的account。实际上我是为了实现Sync Adapter而做的准备工作。目前的需求是这样的,在一个web server上有用户的数据(protected data/api),web server后台是自己用Django做的,安装了oauth toolkit提供OAuth2授权服务。现在待开发的Android App需要实现一个stub authenticator,用来向Android系统添加新用户,并在添加用户过程中与web server进行交互,登录并获取符合OAuth2规范的access token(在Android系统上也称为authToken)。成功添加用户后,Sync Adapter应当可以通过保存在设备上的access token访问位于web server上的用户数据。


由于自己是Android初学,目前仅实现了添加用户和获取access token。尚未实现refresh token的逻辑。这里仅做一个记录,等后期完善后再对本文进行更新(也许是自己骗自己也说不定。。)


目前开发的目标系统是Android API24,web server跑的是Django 1.11,安装了oauth toolkit。Android App利用Retrofit库实现对web server的访问(在OAuth2的定义下,这个server也是auhorization server)。调试时server和Android App都运行在本地,本机局域网ip地址为192.168.123.96,Django跑在8080端口上。


方案

本工作受到了如下博文的启发,并直接使用了博主公开的部分代码。

http://blog.udinic.com/2013/04/24/write-your-own-android-authenticator/


罗列一下所用到的概念、功能和库:

(1)Android要求我们通过继承AbstractAccountAuthenticator来定义我们的Authenticator。

(2)为了能够协助在Android上注册新的用户并获取所需的授权(access token),Android预定义了AccountAuthenticatorActivity作为辅助。

(3)为了能在添加新用户到Android系统上时访问authorization server并提示用户登录和授权access token给App,App需要另外一个Activity来显示网页,这利用了Android的WebView,并且配置了WebViewClient用于过滤http(https)协议或非http(https)协议。访问web server是通过Retrofit完成的。


目前的App在添加account到Android系统方面的设计构成图如下图所示。该图的全尺寸版本位于我的个人主页上。




基于上图对添加account的流程进行说明和解释。红色圈表示流程号,以下用“流程x”指代。

(1)通过继承AbstractAccountAuthenticator获得Authenticator类,Android要求派生类override多个方法,这里主要实现addAccount()和getAuthToken()方法,其他方法为默认形式。

(2)在Android系统的Settings->Accounts里,点击Add account,之后通过点击本App的列表条目时(如下图所示)将从Android系统的account manager发出一个调用,调用Authenticator的addAccount()方法,如流程1。目前App向Android系统注册的用户类型名为“huyaoyu.com",是我的个人主页地址,图标仅是一个png图片。



(3)Authenticator类将配套一个Activity,这个Activity将从AccountAuthenticatorActivity派生而来,之所以从AccountAuthenticatiorActivity派生,是因为它提供了setAccountAuthenticatorResult()方法和finish()方法,这些是Android期待的预定义行为。该Activity称为LoginActivity。LoginActivity从Android studio提供的LoginActivity修改而来,如下图所示,将activity原来的父类从AppCompatActivity修改为AccountAuthenticatorActivity,并增加了一个”用户名“EditText。


(4)如流程2,Authenticator的addAccount()方法将返回一个Bundle,内有一个Intent。这个Intent指定了使用App的LoginActivity作为处理该Intent的activity。此时Android系统将调用LoginActivity,如流程3

(5)用户在LoginActivity上输入必要的user credentials,例如用户名,email和密码。之后用户点击activity上的按钮(将从按钮的on click listener 内调用attemptLogin(),进而调用sendLoginIntent()函数),提交request给web server。

(6)本来从LoginActivity应当直接提交一个登录request给web server,然后web server验证登录信息后再提示用户授权App。但是目前的调试中并未实现该流程,而是显示了一个web页面,需要用户再次输入用户名和密码来登录服务器。这里将会在以后做出调整。(呵呵哒)

(7)LoginActivity从CredentialInfo类中获取与web server和本App直接相关的client ID,client secret以及redirect uri。这些信息是预先在web server上通过访问applications页面注册App而得到的,具体流程可参考我的另一篇博客

(8)为了能够在web server返回access token时,LoginActivity仍能保有有效的用户信息,LoginActivity提交登录请求时使用了一个新的activity而不是使用Android系统的浏览器,该activity称为LoginWebActivity。如流程4,LoginActivity通过一个Intent显式调用LoginWebActivity。在这个Intent的data属性中保存着需要访问的URL。

(9)LoginWebActivity内只有一个WebView,用于显示用户登录web server的页面。LoginWebActivity从Intent中获取URL,并向web server发出GET request,如流程5

(10)Web server响应请求,将登录页面返回给LoginWebActivity的WebView控件。用户得到WebView渲染好的网页,输入自己的用户名和密码进行登录,如下图。


之后web server仍然通过WebView显示一个新的授权页面。用户需要选择同意授权,如下图。此时web server充当authorization server的角色,将会根据OAuth2的要求,向Android系统返回一个authorization code,如流程6。此时LoginWebActivity的onResume()函数将响应这个response,从response中提取出该authorization code,并立即使用这个authorization code再次从web server请求一个access token,如流程7


(11)web server检验Android系统提交的request中信息的有效性,当信息有效时,将access token等信息发送回给指定的redirect uri,如流程8

(12)若做任何处理,此时默认的行为本应是LoginWebActivity的WebView继续尝试访问redirect uri指定的位置。但是在实际业务流程里,不希望LoginWebActivity处理对access token的接受,而是希望由LoginWebActivity处理。于是在LoginWebActivity的WebView上需要配置一个WebViewClient对象用于过滤uri中的sheme。这里是通过WebViewClient的派生类tokenWebViewClient实现的。tokenWebViewClient使得LoginWebActivity只对http(https) scheme做出响应,而忽略其他scheme。web server指定的redirect uri的scheme不是http或https,而是huyaoyuauth。关于redirect uri和shceme的概念,请参考我的另一篇博客

(13)根据目前LoginActivity的设计,它具备一个intent-filter,来响应web server返回的redirect uri。也就是对流程9进行响应。实际响应流程9的接口为LoginActivity的onResume()函数。onResume()函数提取web server发来的access token和其他关键信息,之后创建一个新的accout,并将access token等信息存储在这个accout内。利用继承来的setAccountAuthenticatorResult()函数和finish()函数完成account的添加。

(14)LoginActivity停止,将返回Android的account manager,流程10。添加好account后,Android系统的Settings->Accounts列表下将出现相应的account,如下图所示。


(15)为了测试新添加用户的可用性,设计了一个MainActivity,其layout上有一个button。点击该button时将尝试通过AccountManager来获取刚刚添加的那个用户的authToken(也就是刚刚获取到的access token)。

(16)测试时MainActivity通过getAuthTokenByFeatures()(在getTokenForAccountCreateIfNeeded()中调用,这个函数尚未完全完成create的功能)向Android系统发出查询请求,要求系统查找符合特定要求的用户,流程11


(17)Android系统查找到相应的accout,若对应于该account,Android系统上已经存储过authToken,则直接返回该authToken。若Android系统长没有valid的authToken时,通过调用Authenticator的getAuthToken()函数来获取authToken。由于在流程9中已经通过setAuthToken()函数设置过authToken,Android系统此时不会调用Authenticator的getAuthToken()函数。当系统使用AccountManager.invalidAuthToken()显式地令一个authToken失效后,再次获取authToken时就会调用Authenticator的getAuthToken()函数。

(18)正确获得到authToken后,返回给Android系统,流程13。Android系统将结果返回给MainActivity,流程14




参考

http://blog.udinic.com/2013/04/24/write-your-own-android-authenticator/

https://developer.android.com/training/id-auth/authenticate.html

https://developer.android.com/training/sync-adapters/creating-authenticator.html#DeclareAuthenticator


实现细节,tips

在LoginActivity里增加”用户名“EditText的原因

Django的admin默认使用用户名作为主键。


用户在流程5中显示的网页上输入用户名和密码登录后,如何能够将用户名显示在随后的授权网页上?

替换oauth toolkit的默认template。该template原本位于

python3.5/site-packages/oauth2_provider/templates/oauth2_provider/authorize.html

复制该template到目前server开发路径上的模板文件夹内。模板文件夹的位置可通过Django项目的settings.py设定,变量名为TEMPLATES。在页面上显示当前的用户名,这个变脸是存储在user.username内。

参考

https://django-oauth-toolkit.readthedocs.io/en/latest/templates.html#

https://docs.djangoproject.com/en/1.11/topics/templates/


如何使用Django的默认login页面?

流程5的WebView中显示的是Django的默认accounts/login,在GET请求中需要明确指定next变量。这个next变量将替换Django 的login页面模板中的next表单域的值。这个next表单(隐藏表单)用于成功登录后定向到oauth toolkit的authorize页面。next的值最初是在LoginActivity的sendLoginIntent()函数内生成的,过程中使用了java.net.URLEncoder库。


Authenticator Service

为了能够在流程1中使Android系统能够正确找到对应于huyaoyu.com类型的用户创建时所使用的类,需要设计一个Authenticator Service用以配合从AbstractAccountAuthenticator派生而来的Authenticator类。这需要一个JAVA class和一个xml资源文件。可参考

https://developer.android.com/training/id-auth/custom_auth.html


AbstractAccountAuthenticator类和AccountAuthenticatorActivity类的关系

在AbstractAccountAuthenticator类的派生类Authenticator类的addAccount()函数中,需要将一个Intent放置于一个Bundle中返回。这个Intent必须包含一个作为参数传递进addAccount()来的response。这在Android的开发说明中已经强调了,

https://developer.android.com/reference/android/accounts/AccountAuthenticatorActivity.html

在AccountAuthenticatorActivity的派生类LoginActivity结束添加account前,需要显式调用 setAccountAuthenticatorResult()函数。


使用WebViewClient对象来对scheme进行过滤

流程8中提到,需要使用WebViewClient对象实现对http(https)scheme和非http(https) scheme进行过滤,这需要使用到shouldOverrideUrlLoading()。目前使用的Android SDK的WebViewClient类提供了两个shouldOverrideUrlLoading()的接口。实际测试时发现只有一个是可用的。参考

https://stackoverflow.com/questions/36484074/is-shouldoverrideurlloading-really-deprecated-what-can-i-use-instead


在WebView中使能JavaScript

在进行网页上的授权操作时(流程5、6)需要使用JavaScript,WebView默认情况下是关闭JavaScript支持的。使能JavaScript的方法参考

https://developer.android.com/guide/webapps/webview.html#UsingJavaScript


Android Permission

在测试时遇到了几次Android系统授权错误,于是在manifest文件中一股脑添加了几个permission,这些包括

    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.READ_PROFILE" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>

参考

https://developer.android.com/training/id-auth/authenticate.html


20180317更新

将获取authToken的测试过程修改为在点击GET AUTHTOKEN按钮后,启动一个AsyncTask,在AsyncTask的doInBackground()函数中,通过AccountManager.blockingGetAuthToken()函数获取authToken。并根据authToken的时效来判断是否需要链接服务器来refresh token。成功获取有效的authToken后,将在AsyncTask的onPostExecute()函数中对MainActivity的TextView对象进行内容更新。使用AsyncTask获取authToken的流程如下图所示。原尺寸图片位于我的个人网站

Flowchart of AsyncTask

oauth toolkit执行refresh token的方法参考了

https://stackoverflow.com/questions/26903087/how-to-use-refresh-token-for-getting-new-access-tokendjango-oauth-toolkit


源码

本实例的源码已共享在我的GitHub上。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值