博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android + Django + OAuth2 + Stub Authenticator
阅读量:2134 次
发布时间:2019-04-30

本文共 6708 字,大约阅读时间需要 22 分钟。

概况

最近研究了一下如何在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端口上。

方案

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

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

(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

参考

实现细节,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内。

参考

如何使用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资源文件。可参考

AbstractAccountAuthenticator类和AccountAuthenticatorActivity类的关系

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

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

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

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

在WebView中使能JavaScript

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

Android Permission

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

    

参考

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的方法参考了

源码

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

你可能感兴趣的文章
linux之CentOS下文件解压方式
查看>>
Django字段的创建并连接MYSQL
查看>>
div标签布局的使用
查看>>
HTML中表格的使用
查看>>
(模板 重要)Tarjan算法解决LCA问题(PAT 1151 LCA in a Binary Tree)
查看>>
(PAT 1154) Vertex Coloring (图的广度优先遍历)
查看>>
(PAT 1115) Counting Nodes in a BST (二叉查找树-统计指定层元素个数)
查看>>
(PAT 1143) Lowest Common Ancestor (二叉查找树的LCA)
查看>>
(PAT 1061) Dating (字符串处理)
查看>>
(PAT 1118) Birds in Forest (并查集)
查看>>
数据结构 拓扑排序
查看>>
(PAT 1040) Longest Symmetric String (DP-最长回文子串)
查看>>
(PAT 1145) Hashing - Average Search Time (哈希表冲突处理)
查看>>
(1129) Recommendation System 排序
查看>>
PAT1090 Highest Price in Supply Chain 树DFS
查看>>
(PAT 1096) Consecutive Factors (质因子分解)
查看>>
(PAT 1019) General Palindromic Number (进制转换)
查看>>
(PAT 1073) Scientific Notation (字符串模拟题)
查看>>
(PAT 1080) Graduate Admission (排序)
查看>>
Play on Words UVA - 10129 (欧拉路径)
查看>>