详解从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中文件遍历的两种方法
Jun 16 Python
Python压缩和解压缩zip文件
Feb 14 Python
使用Python脚本将Bing的每日图片作为桌面的教程
May 04 Python
Python正则表达式使用经典实例
Jun 21 Python
python实现验证码识别功能
Jun 07 Python
让代码变得更易维护的7个Python库
Oct 09 Python
对python中的 os.mkdir和os.mkdirs详解
Oct 16 Python
python Tkinter的图片刷新实例
Jun 14 Python
python的列表List求均值和中位数实例
Mar 03 Python
python根据完整路径获得盘名/路径名/文件名/文件扩展名的方法
Apr 22 Python
在TensorFlow中实现矩阵维度扩展
May 22 Python
Django+Uwsgi+Nginx如何实现生产环境部署
Jul 31 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实现计算两个日期间隔的年、月、周、日数
2013/06/13 PHP
php版小黄鸡simsimi聊天机器人接口分享
2014/01/26 PHP
PHP利用header跳转失效的解决方法
2014/10/24 PHP
PHP实现根据数组的值进行分组的方法
2017/04/20 PHP
修改Laravel自带的认证系统的User类的命名空间的步骤
2019/10/15 PHP
关于__defineGetter__ 和__defineSetter__的说明
2007/05/12 Javascript
JavaScript 学习笔记一些小技巧
2010/03/28 Javascript
基于jQuery的前端数据通用验证库
2011/08/08 Javascript
浏览器窗口大小变化时使用resize事件对框架不起作用的解决方法
2014/05/11 Javascript
JavaScript模板引擎Template.js使用详解
2016/12/15 Javascript
ES6关于Promise的用法详解
2018/05/07 Javascript
vue router 跳转后回到顶部的实例
2018/08/31 Javascript
vue 中 命名视图的用法实例详解
2019/08/14 Javascript
Python脚本在Appium库上对移动应用实现自动化测试
2015/04/17 Python
Python探索之静态方法和类方法的区别详解
2017/10/27 Python
python使用xslt提取网页数据的方法
2018/02/23 Python
详解tensorflow载入数据的三种方式
2018/04/24 Python
Python基于win32ui模块创建弹出式菜单示例
2018/05/09 Python
使用Python读取二进制文件的实例讲解
2018/07/09 Python
Python字典添加,删除,查询等相关操作方法详解
2020/02/07 Python
浅谈python多线程和多线程变量共享问题介绍
2020/04/17 Python
Python如何对XML 解析
2020/06/28 Python
python+selenium爬取微博热搜存入Mysql的实现方法
2021/01/27 Python
pycharm 实现调试窗口恢复
2021/02/05 Python
五个2015 年最佳HTML5 框架
2015/11/11 HTML / CSS
Homestay中文官网:全球寄宿家庭
2018/10/18 全球购物
Omio俄罗斯:一次搜索公共汽车、火车和飞机的机票
2018/11/17 全球购物
德国富尔达运动鞋店:43einhalb
2020/12/25 全球购物
高中毕业自我鉴定
2013/12/19 职场文书
自我鉴定写作要点
2014/01/17 职场文书
中学生期末评语
2014/02/03 职场文书
2014年秋季开学寄语
2014/08/02 职场文书
大学考试作弊检讨书
2015/05/06 职场文书
法律意见书范文
2015/06/04 职场文书
优秀范文:《但愿人长久》教学反思3篇
2019/10/24 职场文书
vue3中的组件间通信
2021/03/31 Vue.js