详解从Django Allauth中进行登录改造小结


Posted in Python onDecember 18, 2019

大概来介绍一下 Django Allauth 改造的期间遇到的一些问题和改造方法,在此之前我只想说——Django Allauth 是屑。

为什么我说 Django Allauth 是屑

入职之初我就接到了一些第三方登录的任务,然而 Django Allauth 将内部封装的太好,暴露的 API 不足,更新又慢,issue 和 PR 很少有人处理,当你需要扩展时,很多情况下你只能用一些 hack 的手段去解决问题,非常蛋疼,所以当时就决定慢慢的切到自己的一套 Auth 体系中。

目前已经做的是第三方登录的部分,账号管理的部分还没有迁移,之前稍微看了一下,要迁移的成本还是比较麻烦的。

迁移成本在哪里

Django 中的账号密码登录一般是由本身提供的 auth 表进行扩展的结果,而 allauth 在此基础上扩充了第三方登录的几个表,再和本身的 auth user 表关联。而这一部分是构建在 Allauth 内部的 model 内,且没有暴露任何的方法来修改结构(当然可能也是因为真的不好改),导致一旦不满足需求就很难搞,因为数据已经放在那里了,刷数据同步的方案对于大流量网站来说也并不是很友好的选择。

此外,在路由上,由于我们需要尽可能的无痛迁移和在渐进式切换时的平稳降级,因此只能通过简单粗暴的路由覆盖操作,这极度依赖路由的解析顺序。

数据库扩展与 provider 变更

说了这么多,其实关键点并不在于「问题在哪里」,而在于「我是怎么解决这些问题的」。

Allauth 一个平台的注册是一个 provider,比如 「wechat」、「weibo」、「qq」,整张表是一对一的关系,那么问题来了,我们知道,国内的平台往往并不是一个 appid 和 key 能搞定的事情,对于 web 和移动端的平台来说,其实是两个 appid 共享一套 unionid,尽管官方提供了一套增加 Provider 的扩展方式,但实际上是没有必要的,因为 Web 和移动端来说,获得用户信息的接口是共享的,而移动端并不用通过后端获取 access_token。在绑定上,实际上也是同一个平台。

因此我们扩充了一张表来解决这个问题,将我们额外的信息放在了额外添加的表中。

之后要解决的就是 admin 的 provider select 问题,它会进行一次校验,所以我们必须要取消这些校验并把 select 改成 input。

首先,我们要取消 Model 层的校验, Proxy 可以对表进行一些覆盖式的操作(但不能改变表结构):

class CustomSocialApp(SocialApp):
  class Meta:
    proxy = True

  def clean_fields(self, exclude=None):
    # 别校验了
    pass

  def full_clean(self, exclude=None, validate_unique=True):
    # 别校验了
    pass

  def clean(self, exclude=None, validate_unique=True):
    # 别校验了
    pass

这里我们在原来的 SocialApp 的基础上新建一个属于自己的新的 Admin,他本质上还是操作 SocialApp 表,只是挪出来方便我们自定义而已:

class CustomSocialAppAdmin(SocialAppAdmin):
  list_display = ('provider_text', 'name')
  form = CustomAppAdminForm

  def get_form(self, request, obj=None, **kwargs):
    kwargs['widgets'] = {'provider': forms.TextInput}
    return super().get_form(request, obj, **kwargs)

  def provider_text(self, obj):
    return obj.provider

但是这样就会遇到一个 provider 的校验问题,这也就是上面我们还没有写完的 CustomAppAdminForm 的部分,我们将校验的部分用自定义的 form 完全取消:

class CustomSocialAppAdminForm(forms.ModelForm):
  class Meta:
    model = CustomSocialApp
    fields = '__all__'
    widgets = {'provider': forms.TextInput()}

  def clean(self):
    # 别校验了
    if self.has_error('provider'):
      del self._errors['provider']
    self.cleaned_data['provider'] = self.data['provider']
    return self.cleaned_data

这样就完成了校验的修改,成了一个完全体的 input 覆盖了原来的 select。

第三方登录与绑定流程

上面可以任意在表中拓展 provider 了 ,但重头戏其实是:搞清楚 allauth 原本的登录和绑定流程,完美的 copy 一份流程,这样才能实现平稳降级和无痛迁移。

查找账号

  1. 获取用户授权信息中的 uid
  2. 在 AllauthSocialAccount 表中获取到对应的数据,如果没有则返回 None

登录流程

  1. 确保用户是匿名用户:request.user.is_anouymous 且已经存在对应的账号
  2. 更新 AllauthSocialAccount 表中的数据到最新
  3. 根据 social account 更新 social token
  4. 写入 session(Django 中自带 login 函数)

注册流程

  1. 确保用户是匿名用户且不存在对应账号
  2. 创建新用户(要点是生成用户名和昵称),在 Django 中有 create_user 可以直接创建
  3. 写入 AllatuhSocialAccount 和 AllauthSocialToken
  4. 写入 session 登录

绑定流程

  • 用户不是匿名用户
  • 查找对应的第三方账号是否已经被绑定
  • 更新 AllauthSocialAccount 表
  • 更新 social token

只要按照这个流程实现下来就可以了,而同一平台多 provider(appid)的差异功能与核心部分无关,可以在各社交媒体对应的文件中单独实现。

构建新的账号系统

现在我们彻底将第三方登录抽离了出来,接下来需要抽出账号的部分,账号登录和注册本质上还是 Django 提供的那些东西,因此比较好抽,需要兼容的部分主要在于「忘记密码」和「重置密码」。

我们来思考一下为什么这部分需要做兼容:

一般来说我们都是在重置密码时在手机或者邮箱里收到一个验证邮件,里面会附上一个随机字符串用来保证连接的唯一性。而在我们替换过程中,我们不能让一群用户已经发送过但还没有使用的随机字符串不可用,从可读的角度来看,生成的内容也应该和原来差不多(同时也是避免冲突),因此需要抄一下它的忘记密码。

在 account/forms 中表明了 token 的生成算法:

from django.contrib.auth.tokens import PasswordResetTokenGenerator
token_generator = PasswordResetTokenGenerator()
# 生成 token
key = token_generator.make_token(user)
# 检查 token
token_generator.check_token(user, key)

Allauth 中将 user 用 base36 加密了,兼容 Python2,所以 utils 中的语句略长,由于我们直接是 Python3,所以只剩下这些句子:

from django.utils.http import base36_to_int
from django.utils.http import int_to_base36


def user_pk_to_url_str(user):
  return int_to_base36(user.pk)


def url_str_to_user_pk(s):
  return base36_to_int(s)

所有内容将会被存储在 account_emailconfirmation 表中,这样就能保证对应的关系了。

总结

在账号的部分由于还没有改完,所以可以说的不多,只是做了一些微小的工作,对于这种可能需要根据国情定制的需求,建议大家还是小心使用。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
使用Protocol Buffers的C语言拓展提速Python程序的示例
Apr 16 Python
小小聊天室Python代码实现
Aug 17 Python
梯度下降法介绍及利用Python实现的方法示例
Jul 12 Python
Python 实现「食行生鲜」签到领积分功能
Sep 26 Python
python 处理telnet返回的More,以及get想要的那个参数方法
Feb 14 Python
python中如何使用分步式进程计算详解
Mar 22 Python
python中正则表达式与模式匹配
May 07 Python
python基础教程之while循环
Aug 14 Python
python 列表推导式使用详解
Aug 29 Python
Python小程序之在图片上加入数字的代码
Nov 26 Python
python opencv根据颜色进行目标检测的方法示例
Jan 15 Python
opencv读取视频并保存图像的方法
Jun 04 Python
解决pycharm最左侧Tool Buttons显示不全的问题
Dec 17 #Python
python 字段拆分详解
Dec 17 #Python
从pandas一个单元格的字符串中提取字符串方式
Dec 17 #Python
基于pandas中expand的作用详解
Dec 17 #Python
Python使用psutil获取进程信息的例子
Dec 17 #Python
python psutil监控进程实例
Dec 17 #Python
查看端口并杀进程python脚本代码
Dec 17 #Python
You might like
php中访问修饰符的知识点总结
2019/01/27 PHP
用Div仿showModalDialog模式菜单的效果的代码
2007/03/05 Javascript
基于jquery的动态创建表格的插件
2011/04/05 Javascript
nodejs npm包管理的配置方法及常用命令介绍
2014/06/05 NodeJs
使用Node.js实现一个简单的FastCGI服务器实例
2014/06/09 Javascript
使用C++为node.js写扩展模块
2015/04/22 Javascript
JS组件Bootstrap Table表格行拖拽效果实现代码
2020/08/27 Javascript
Javascript的表单验证-提交表单
2016/03/18 Javascript
js实现弹窗居中的简单实例
2016/10/09 Javascript
JavaScript实现网页头部进度条刷新
2017/04/16 Javascript
详解node中创建服务进程
2017/05/09 Javascript
jQuery实现下拉菜单的实例代码
2017/06/19 jQuery
详解angularjs实现echart图表效果最简洁教程
2017/11/29 Javascript
vue iview多张图片大图预览、缩放翻转
2019/07/13 Javascript
微信小程序如何引用外部js,外部样式,公共页面模板
2019/07/23 Javascript
Node.js HTTP服务器中的文件、图片上传的方法
2019/09/23 Javascript
vue 将多个过滤器封装到一个文件中的代码详解
2020/09/05 Javascript
布同 Python中文问题解决方法(总结了多位前人经验,初学者必看)
2011/03/13 Python
Python基础学习之常见的内建函数整理
2017/09/06 Python
python去除拼音声调字母,替换为字母的方法
2018/11/28 Python
简单了解Python3里的一些新特性
2019/07/13 Python
使用Puppeteer爬取微信文章的实现
2020/02/11 Python
linux 下selenium chrome使用详解
2020/04/02 Python
Django 实现 Websocket 广播、点对点发送消息的代码
2020/06/03 Python
Python常用数字处理基本操作汇总
2020/09/10 Python
美国亚马逊旗下男装网站:East Dane(支持中文)
2019/09/25 全球购物
应届毕业生个人求职自荐信
2014/01/06 职场文书
工伤赔偿协议书
2014/04/15 职场文书
村庄绿化方案
2014/05/07 职场文书
索赔员岗位职责
2015/02/15 职场文书
职称评定个人总结
2015/03/05 职场文书
盲山观后感
2015/06/11 职场文书
关于法制教育的宣传语
2015/07/13 职场文书
Golang的继承模拟实例
2021/06/30 Golang
Java并发编程必备之Future机制
2021/06/30 Java/Android
国产动画《万圣街》日语配音版制作决定!
2022/03/20 国漫