详解从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 相关文章推荐
python模块restful使用方法实例
Dec 10 Python
python解析xml文件操作实例
Oct 05 Python
python 远程统计文件代码分享
May 14 Python
基于Django统计博客文章阅读量
Oct 29 Python
详解字符串在Python内部是如何省内存的
Feb 03 Python
python利用paramiko实现交换机巡检的示例
Sep 22 Python
python基于pygame实现飞机大作战小游戏
Nov 19 Python
Django REST Framework 分页(Pagination)详解
Nov 30 Python
python opencv图像处理(素描、怀旧、光照、流年、滤镜 原理及实现)
Dec 10 Python
python推导式的使用方法实例
Feb 28 Python
Python解析m3u8拼接下载mp4视频文件的示例代码
Mar 03 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定义数组和使用示例(php数组的定义方法)
2014/03/29 PHP
让CodeIgniter数据库缓存自动过期的处理的方法
2014/06/12 PHP
php实现的操作excel类详解
2016/01/15 PHP
PHP用PDO如何封装简单易用的DB类详解
2017/07/30 PHP
Javascript 模式实例 观察者模式
2009/10/24 Javascript
extjs DataReader、JsonReader、XmlReader的构造方法
2009/11/07 Javascript
JavaScript创建类/对象的几种方式概述及实例
2013/05/06 Javascript
new Date()问题在ie8下面的处理方法
2014/07/31 Javascript
PHP结合jQuery实现的评论顶、踩功能
2015/07/22 Javascript
Javascript编程之继承实例汇总
2015/11/28 Javascript
基于Bootstrap的Java开发问题汇总(Spring MVC)
2017/01/15 Javascript
angular实现表单验证及提交功能
2017/02/01 Javascript
Node.js与Sails redis组件的使用教程
2017/02/14 Javascript
Vue2递归组件实现树形菜单
2017/04/10 Javascript
详解Vue 2.0封装axios笔记
2017/06/22 Javascript
原生javascript实现的全屏滚动功能示例
2017/09/19 Javascript
JavaScript DOM元素常见操作详解【添加、删除、修改等】
2018/05/09 Javascript
详解vue添加删除元素的方法
2018/06/30 Javascript
详解如何在vscode里面调试js和node.js的方法步骤
2018/12/24 Javascript
vue路由切换之淡入淡出的简单实现
2019/10/31 Javascript
[56:24]DOTA2上海特级锦标赛主赛事日 - 3 胜者组第二轮#1Liquid VS MVP.Phx第二局
2016/03/04 DOTA
[43:03]完美世界DOTA2联赛PWL S2 PXG vs Magma 第二场 11.21
2020/11/24 DOTA
简单解决Python文件中文编码问题
2015/11/22 Python
Python+matplotlib+numpy实现在不同平面的二维条形图
2018/01/02 Python
Django 模型类(models.py)的定义详解
2019/07/19 Python
简单了解python数组的基本操作
2019/11/26 Python
python excel和yaml文件的读取封装
2021/01/12 Python
基于pycharm 项目和项目文件命名规则的介绍
2021/01/15 Python
使用CSS3代码绘制可爱的Hello Kitty猫
2016/08/03 HTML / CSS
CSS3 实现的火焰动画
2020/12/07 HTML / CSS
详解基于 Canvas 手撸一个六边形能力图
2019/09/02 HTML / CSS
Java中有几种类型的流?JDK为每种类型的流提供了一些抽象类以供继承,请说出他们分别是哪些类
2012/02/06 面试题
社区端午节活动总结
2015/02/11 职场文书
Python基础之常用库常用方法整理
2021/04/30 Python
ObjectMapper 如何忽略字段大小写
2021/06/29 Java/Android
Redis高可用集群redis-cluster详解
2022/03/20 Redis