Django 用户模块扩展

Django | User Model | 用户模块扩展 | 用户模块迁移 | 彻底扩展 | 项目前期/中期扩展方法

Posted by luoruiqing on September 14, 2020

前言

Django 有很多自身的模块和扩展模块都需要依赖默认的用户系统, 例如 django-notifications-hq, django-guardian 等模块, 这些模块带来的功能非常便利, 如果你没有使用 Django 默认的用户系统, 则与这些优秀的第三方模块无缘了, 当然及时回头也是可以对接使用的, 这里讲下 Django 常见的用户扩展的方式, 请根据自己的情况对号入座:

  • 直接扩展: 适合项目开始之前, 扩展彻底, 维护方便
  • 延伸扩展: 适合项目已经维护了一段时间, 又不想对已有系统有较大改动
  • 覆盖扩展: 适合项目已经维护了一段时间, 对历史迁移破坏较大, 但扩展比较彻底.

直接扩展

适合项目开始之前

1. 定义用户模型

1
2
3
4
5
6
7
8
from django.contrib.auth.models import AbstractUser
from django.db import models


class User(AbstractUser): # 继承老的用户模型
    phone = models.CharField(max_length=20, null=True, blank=True, help_text='手机号码')

    # USERNAME_FIELD = ['phone'] 这个设置用于用户模块的唯一键, 用来区分用户的唯一标识

2. 设置 Admin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.utils.translation import gettext_lazy as _
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as UserAdminBase
from .models import User


@admin.register(User)
class UserAdmin(UserAdminBase):
    ''' 用户视图 '''
    list_display = UserAdminBase.list_display + ('phone',) # 扩展列表页展示字段
    fieldsets = UserAdminBase.fieldsets + (
        (_('用户信息'), {'fields': ('phone',)}),
    ) # 扩展展示字段

3. 设置关系

指明 Django 用户模块所在的应用和模型位置

1
2
# settings.py
AUTH_USER_MODEL = "<应用>.User"

4. 迁移验证

1
./manage.py makemigrations <应用> &&  ./manage.py migrate <应用>

img

延伸扩展

延伸扩展不会对系统造成破坏性, 只是需要注意替换一些模块和代码, 略微提升一点复杂性, 便能满足需求

1. 创建扩展Model

这种扩展方式 相当于创建新表 关联 到用户表中, 使用 OneToOneField 的方式连接两个表, 以下的两种方式都是基于Django配置中指定的默认用户模块 settings.AUTH_USER_MODEL 的值来扩展的

1
2
3
4
5
6
7
8
9
10
11
# account/models.py
from django.utils.translation import gettext_lazy as _
from django.db import models
from django.conf import settings
# from django.contrib import auth

class UserInfo(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    # user = models.OneToOneField(auth.get_user_model(), on_delete=models.CASCADE,)

    phone = models.CharField(_('phone'), max_length=20, null=True, blank=True, help_text='手机号码')

2. User Admin 扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# account/admin.py
from django.contrib import admin
from django.contrib import auth
from .models import UserInfo


class UserInfoInline(admin.StackedInline):
    model = UserInfo
    verbose_name_plural = 'info'


class UserAdmin(auth.admin.UserAdmin):
    inlines = auth.admin.UserAdmin.inlines + [UserInfoInline, ]


admin.site.unregister(auth.models.User)  # 取消之前注册的 User Admin
admin.site.register(auth.models.User, UserAdmin)  # 重新注册最新的 User Admin

3. 迁移验证

别忘了在 settings.py 注册扩展的应用

1
./manage.py makemigrations account &&  ./manage.py migrate account

admin 访问查看: 用户模块位置不变, 但内部可编辑的信息增加了, 每次通过 Admin 修改信息则会对应到用户表和附属信息表内.

img

接口验证

1
2
3
4
5
6
7
8
# account/view.py
from django.contrib import auth 
from django.http import HttpResponse


@auth.decorators.login_required
def test_view(request):
    return HttpResponse(f'您的手机号为: {request.user.userinfo.phone}')

浏览器访问视图后得到结果: 您的手机号为: 13212341234

如果显示手机号为空, 请检查是否成功录入了数据; 如果显示404, 那是因为没有登录页面, 可以考虑从 Django Admin 登录后再访问接口视图

覆盖扩展

覆盖扩展将会改变迁移模块的历史记录, 破坏性比较大, 容易导致线上项目运行出现问题, 若必须进行扩展需要考虑多个风险情况, 数据备份, 服务验证测试等等, 酌情使用

全过程步骤

  1. 全库备份
  2. 创建app
  3. 继承 User 模块
  4. 修改配置
  5. 检查代码中使用到 User 模块处需改为新模块
  6. 删除所有的迁移文件
  7. 重新生成迁移文件
  8. 重置迁移记录表
  9. 应用迁移到数据库
  10. 确认是否迁移成功
  11. 扩展 User 模块的字段
  12. 生成迁移文件
  13. 执行迁移

1. 全库备份

若不使用全库备份, 很容易对线上造成难以修复的问题, 建议 全库备份 后进行迁移, 并且停机上线服务. 或者停止写入, 做AB数据库和服务切换的形式维护.

若要继续跟随教程操作, 一定要全库备份! , 以免造成损失.

2. 创建APP

1
2
# ./manage.py startapp <你的APP名称>
./manage.py startapp Accounts

3. User模块

1
2
3
4
5
6
from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser): 
    class Meta:
        db_table = 'auth_user'

这里尽量使用 User 作为Model名称, 显式的指明 db_table 为表名称

继承 User 模块建议第一次迁移不包含扩展字段, 只保留初始结构, 等待成功接入后再扩展字段

4. 修改配置

1
2
3
4
5
6
INSTALLED_APPS = (
        # ...
        'Accounts', # <你的APP名称>
    )

AUTH_USER_MODEL = 'Accounts.User' # <你的APP名称>.<你的用户模型名称>

这里重点是两个配置, INSTALLED_APPS 中注册你的 app, AUTH_USER_MODEL 指明的用户模块对应的 Model

5. 替换User

查找你的代码:

1
from django.contrib.auth.models import User

替换为:

1
2
# 你的模块.User
from Accounts.models import User

6. 清除迁移

1
2
find . -path "*/migrations/*.py" -not -name "__init__.py" -delete
find . -path "*/migrations/*.pyc" -delete

这里会清除你现有系统的所有迁移文件, 以用来保证依赖关系的正确性

7. 生成迁移

1
./manage.py makemigrations

这里是生成所有app的迁移. 在生成时可能发生下面的错误

from .migration import Migration, swappable_dependency  # NOQA
ModuleNotFoundError: No module named 'django.db.migrations.migration'

这个问题可能是使用的沙盒环境导致的, 只需要在沙盒环境下, 重装Django即可, 注意版本

1
2
uninstall Django -y
pip install -i https://pypi.douban.com/simple/ django==<你的Django版本>

迁移后即可生成迁移文件

8. 重置迁移表

1
TRUNCATE TABLE django_migrations;

9. 应用迁移

1
./manage.py migrate --fake

使用 --fake 参数继续

10. 确认是否迁移成功

迁移成功则不会报错, 会有类似下面的日志输出:

Applying contenttypes.0001_initial... FAKED
Applying contenttypes.0002_remove_content_type_name... FAKED
Applying auth.0001_initial... FAKED
Applying auth.0002_alter_permission_name_max_length... FAKED
Applying auth.0003_alter_user_email_max_length... FAKED
Applying auth.0004_alter_user_username_opts... FAKED
Applying auth.0005_alter_user_last_login_null... FAKED
Applying auth.0006_require_contenttypes_0002... FAKED
Applying auth.0007_alter_validators_add_error_messages... FAKED
Applying auth.0008_alter_user_username_max_length... FAKED
Applying auth.0009_alter_user_last_name_max_length... FAKED
Applying auth.0010_alter_group_name_max_length... FAKED
Applying auth.0011_update_proxy_permissions... FAKED
Applying account.0001_initial... FAKED
Applying admin.0001_initial... FAKED
Applying admin.0002_logentry_remove_auto_add... FAKED
Applying admin.0003_logentry_add_action_flag_choices... FAKED

11. 扩展自身的字段

新增扩展的用户字段

1
2
3
4
5
6
7
8
9
class User(AbstractUser):
    name_cn = models.CharField(_('name_cn'), max_length=255, default='', null=True, blank=True, help_text='中文名')
    name_en = models.CharField(_('name_en'), max_length=255, default='',  null=True, blank=True, help_text='英文名')
    phone = models.CharField(_('phone'), max_length=22, default='', null=True, blank=True, help_text='手机号码')
    avatar_url = models.URLField(_('avatar_url'), max_length=255, default='', null=True, blank=True, help_text='头像地址')
    info = JSONField(_('info'), default={}, dump_kwargs={'ensure_ascii': False}, blank=True, help_text='其他信息')

    class Meta(AbstractUser.Meta):
        db_table = 'auth_user'

12. 生成迁移文件

1
./manage.py makemigrations

13. 执行迁移

1
./manage.py migrate --fake

其他认证来源

如需要满足类似 LDAP 认证等场景, 保证其他符合权限要求的的请求可以自动认证, 则需要编写 Backend 用于权限识别.

1. 编写认证模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# account/auth.py
from apps.account.models import UserInfo
from django.contrib import auth


DjangoUser = auth.get_user_model()


def authenticate(request, username=None, password=None, token=None, is_staff=True):
    ''' 你的自定义认证流程, 无论是 LDAP 还是 COOKIE/JWT 信息校验方法 '''
    user_info = {'id': -2, 'phone': '13212341234'}  # 这里获取你认证的结果
    if user_info['id']:  # 认证成功
        # 留存用户到 Django 默认用户表
        django_user, _ = DjangoUser.objects.update_or_create(id=user_info['id'], defaults={
            'is_active': True,  # 设置为可进入用户
            'is_staff': is_staff,  # 设置为可以登录到 Admin 页面
        })
        # 留存扩展信息到扩展表, 若是直接扩展忽略
        UserInfo.objects.update_or_create(user=django_user, defaults={'phone': user_info['phone']})
        auth.login(request, django_user, backend='django.contrib.auth.backends.ModelBackend')
        return django_user


class UserBackend:
    ''' 该认证模块用于认证信息 '''

    def authenticate(self, *args, **kwargs):
        ''' 登录时调用 '''
        return authenticate(*args, **kwargs)

    def get_user(self, user_id):
        ''' 认证成功后总是在调用 '''
        try:
            return DjangoUser.objects.get(pk=user_id)
        except auth.models.User.DoesNotExist:
            return None

2. 配置Backend

1
2
3
4
5
6
# setting.py

AUTHENTICATION_BACKENDS = [
    'account.auth.UserBackend',  # 我的认证模块
    'django.contrib.auth.backends.ModelBackend',  # Django 默认的认证
]

说明:

  • settings.AUTHENTICATION_BACKENDS: 注意该选项的顺序, Django 会由上到下的尝试每个 backend 的验证方法, 一旦有认证成功> 的 backend 则不会执行下一个 backend(这里我们优先使用自定义认证, 所以将 UserBackend 放在第一个位置).
  • backend.authenticate : 如何认证取决于你自身的业务逻辑, 认证成功返回 User, 否则返回 None 交给下个 backend
  • backend.get_user : 登录成功后总是在调用, 若希望增加缓存或及时登出系统, 这里可以做到实时拦截并登出
  • auth.login: 用于测试, 会将认证通过的账户自动登录到 Django 系统中与 Admin 连接. 根据你的实际需要更改这个认证流程.

自定义管理器

如果想实现字段的验证, 长度校验等深度定制用户模块时则需要创建一个自定义用户管理器, 完整的例子: https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#a-full-example