前言
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 <应用>
延伸扩展
延伸扩展不会对系统造成破坏性, 只是需要注意替换一些模块和代码, 略微提升一点复杂性, 便能满足需求
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 修改信息则会对应到用户表和附属信息表内.
接口验证
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 登录后再访问接口视图
覆盖扩展
覆盖扩展将会改变迁移模块的历史记录, 破坏性比较大, 容易导致线上项目运行出现问题, 若必须进行扩展需要考虑多个风险情况, 数据备份, 服务验证测试等等, 酌情使用
全过程步骤
- 全库备份
- 创建app
- 继承
User
模块 - 修改配置
- 检查代码中使用到
User
模块处需改为新模块 - 删除所有的迁移文件
- 重新生成迁移文件
- 重置迁移记录表
- 应用迁移到数据库
- 确认是否迁移成功
- 扩展
User
模块的字段 - 生成迁移文件
- 执行迁移
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 交给下个 backendbackend.get_user
: 登录成功后总是在调用, 若希望增加缓存或及时登出系统, 这里可以做到实时拦截并登出auth.login
: 用于测试, 会将认证通过的账户自动登录到 Django 系统中与 Admin 连接. 根据你的实际需要更改这个认证流程.
自定义管理器
如果想实现字段的验证, 长度校验等深度定制用户模块时则需要创建一个自定义用户管理器, 完整的例子: https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#a-full-example