在Django中创建一个自定义用户模型

264 阅读6分钟

如何用电子邮件字段完全取代Django认证中的用户名字段?

这篇文章逐步解释了如何在Django中创建一个自定义的用户模型,以便电子邮件地址可以被用作主要的用户标识符,而不是用于认证的用户名。

请记住,本帖所概述的过程需要对数据库模式进行重大修改。正因为如此,我们只建议新项目使用这个方法。如果这是一个现有的遗留项目,你可能需要备份数据并重新创建数据库。关于这个问题的更多信息,请查阅以下资源。

  1. Django官方文档中的自定义用户模型替代指南
  2. 迁移到Django的自定义用户模型博文

目标

在本文结束时,你应该能够。

  1. 描述AbstractUser 和 之间的区别。AbstractBaseUser
  2. 解释为什么你在开始一个新的Django项目时应该设置一个自定义的用户模型
  3. 用自定义的用户模型启动一个新的Django项目
  4. 使用电子邮件地址作为主要的用户标识,而不是使用用户名进行认证
  5. 在实现自定义用户模型的过程中,练习测试优先的开发方式

AbstractUser vs AbstractBaseUser

Django的默认用户模型在认证时使用用户名来唯一地识别用户。如果你想使用电子邮件地址,你需要通过子类化AbstractUserAbstractBaseUser 来创建一个自定义的User模型。

选项。

  1. AbstractUser:如果你对用户模型上现有的字段很满意,只是想删除用户名字段,请使用这个选项。
  2. AbstractBaseUser:如果你想通过创建你自己的、全新的用户模型从头开始,请使用这个选项。

我们将在这篇文章中讨论这两个选项,AbstractUserAbstractBaseUser

每个选项的步骤都是一样的。

  1. 创建一个自定义的用户模型和管理器
  2. 更新settings.py
  3. 定制UserCreationFormUserChangeForm 表格
  4. 更新管理员

强烈建议在开始一个新的Django项目时设置一个自定义用户模型。如果没有它,你将需要创建另一个模型(如UserProfile),如果你想给用户模型添加新的字段,就需要用OneToOneField 将其与Django用户模型联系起来。

项目设置

首先,创建一个新的Django项目和一个用户应用程序。

$ mkdir django-custom-user-model && cd django-custom-user-model
$ python3 -m venv env
$ source env/bin/activate

(env)$ pip install Django==3.2.2
(env)$ django-admin startproject hello_django .
(env)$ python manage.py startapp users

请随意将virtualenv和Pip换成PoetryPipenv。更多信息,请查看现代Python环境

不要应用迁移。记住:你必须应用第一次迁移之前创建自定义的用户模型。

将新的应用程序添加到settings.pyINSTALLED_APPS 列表中。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'users',
]

测试

让我们采取一个测试优先的方法。

from django.contrib.auth import get_user_model
from django.test import TestCase


class UsersManagersTests(TestCase):

    def test_create_user(self):
        User = get_user_model()
        user = User.objects.create_user(email='normal@user.com', password='foo')
        self.assertEqual(user.email, 'normal@user.com')
        self.assertTrue(user.is_active)
        self.assertFalse(user.is_staff)
        self.assertFalse(user.is_superuser)
        try:
            # username is None for the AbstractUser option
            # username does not exist for the AbstractBaseUser option
            self.assertIsNone(user.username)
        except AttributeError:
            pass
        with self.assertRaises(TypeError):
            User.objects.create_user()
        with self.assertRaises(TypeError):
            User.objects.create_user(email='')
        with self.assertRaises(ValueError):
            User.objects.create_user(email='', password="foo")

    def test_create_superuser(self):
        User = get_user_model()
        admin_user = User.objects.create_superuser(email='super@user.com', password='foo')
        self.assertEqual(admin_user.email, 'super@user.com')
        self.assertTrue(admin_user.is_active)
        self.assertTrue(admin_user.is_staff)
        self.assertTrue(admin_user.is_superuser)
        try:
            # username is None for the AbstractUser option
            # username does not exist for the AbstractBaseUser option
            self.assertIsNone(admin_user.username)
        except AttributeError:
            pass
        with self.assertRaises(ValueError):
            User.objects.create_superuser(
                email='super@user.com', password='foo', is_superuser=False)

将规范添加到users/tests.py,然后确保测试失败。

模型管理器

首先,我们需要添加一个自定义的管理器,通过子类化BaseUserManager ,它使用电子邮件作为唯一的标识符,而不是用户名。

在 "users "目录下创建一个managers.py文件。

from django.contrib.auth.base_user import BaseUserManager
from django.utils.translation import ugettext_lazy as _


class CustomUserManager(BaseUserManager):
    """
    Custom user model manager where email is the unique identifiers
    for authentication instead of usernames.
    """
    def create_user(self, email, password, **extra_fields):
        """
        Create and save a User with the given email and password.
        """
        if not email:
            raise ValueError(_('The Email must be set'))
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, password, **extra_fields):
        """
        Create and save a SuperUser with the given email and password.
        """
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_active', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError(_('Superuser must have is_staff=True.'))
        if extra_fields.get('is_superuser') is not True:
            raise ValueError(_('Superuser must have is_superuser=True.'))
        return self.create_user(email, password, **extra_fields)

决定你想使用哪个选项:subclassingAbstractUserAbstractBaseUser

AbstractUser

更新users/models.py

from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import ugettext_lazy as _

from .managers import CustomUserManager


class CustomUser(AbstractUser):
    username = None
    email = models.EmailField(_('email address'), unique=True)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    objects = CustomUserManager()

    def __str__(self):
        return self.email

在这里,我们

  1. 创建了一个名为CustomUser 的新类,该类的子类为AbstractUser
  2. 删除了username 字段
  3. 使得email 字段成为必需的和唯一的
  4. USERNAME_FIELD --它定义了User 模型的唯一标识符--设置为email
  5. 指定该类的所有对象来自于CustomUserManager

抽象基础用户

更新users/models.py

from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from .managers import CustomUserManager


class CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(_('email address'), unique=True)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    date_joined = models.DateTimeField(default=timezone.now)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    objects = CustomUserManager()

    def __str__(self):
        return self.email

在这里,我们

  1. 创建了一个名为CustomUser 的新类,该类的子类为AbstractBaseUser
  2. 添加了email,is_staff,is_active, 和 的字段。date_joined
  3. USERNAME_FIELD -- 定义了User 模型的唯一标识符 -- 设置为email
  4. 指定该类的所有对象来自于CustomUserManager

设置

settings.py文件中添加以下一行,这样Django就知道要使用新的User类。

AUTH_USER_MODEL = 'users.CustomUser'

现在,你可以创建并应用迁移,这将创建一个使用自定义用户模型的新数据库。在这之前,让我们看看在没有创建迁移文件的情况下,使用--dry-run标志,迁移的实际情况会是怎样的。

(env)$ python manage.py makemigrations --dry-run --verbosity 3

你应该看到类似的东西。

# Generated by Django 3.2.2 on 2021-05-12 20:43

from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('auth', '0012_alter_user_first_name_max_length'),
    ]

    operations = [
        migrations.CreateModel(
            name='CustomUser',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('password', models.CharField(max_length=128, verbose_name='password')),
                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
                ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
                ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
                ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
                ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
                ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
                ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')),
                ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
                ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
            ],
            options={
                'verbose_name': 'user',
                'verbose_name_plural': 'users',
                'abstract': False,
            },
        ),
    ]

如果你选择了AbstractBaseUser ,你就不会有first_namelast_name 的字段。为什么?

请确保迁移中不包括username 字段。然后,创建并应用该迁移。

(env)$ python manage.py makemigrations
(env)$ python manage.py migrate

查看模式。

$ sqlite3 db.sqlite3

SQLite version 3.28.0 2019-04-15 14:49:49
Enter ".help" for usage hints.

sqlite> .tables

auth_group                         django_migrations
auth_group_permissions             django_session
auth_permission                    users_customuser
django_admin_log                   users_customuser_groups
django_content_type                users_customuser_user_permissions

sqlite> .schema users_customuser

CREATE TABLE IF NOT EXISTS "users_customuser" (
  "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
  "password" varchar(128) NOT NULL,
  "last_login" datetime NULL,
  "is_superuser" bool NOT NULL,
  "first_name" varchar(150) NOT NULL,
  "last_name" varchar(150) NOT NULL,
  "is_staff" bool NOT NULL,
  "is_active" bool NOT NULL,
  "date_joined" datetime NOT NULL,
  "email" varchar(254) NOT NULL UNIQUE
);

如果你走的是AbstractBaseUser 的路线,为什么last_login 是模型的一部分?

现在你可以用get_user_model()settings.AUTH_USER_MODEL 来引用用户模型。更多信息请参考官方文档中的 "引用用户模型"。

另外,当你创建一个超级用户时,你应该被提示输入一个电子邮件而不是用户名。

(env)$ python manage.py createsuperuser

Email address: test@test.com
Password:
Password (again):
Superuser created successfully.

请确保测试通过。

(env)$ python manage.py test

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.282s

OK
Destroying test database for alias 'default'...

表格

接下来,让我们对UserCreationFormUserChangeForm 表单进行子类化,使它们使用新的CustomUser 模型。

在 "users "中创建一个新的文件,叫做forms.py

from django.contrib.auth.forms import UserCreationForm, UserChangeForm

from .models import CustomUser


class CustomUserCreationForm(UserCreationForm):

    class Meta:
        model = CustomUser
        fields = ('email',)


class CustomUserChangeForm(UserChangeForm):

    class Meta:
        model = CustomUser
        fields = ('email',)

管理员

通过在users/admin.py 中子类化UserAdmin 来告诉管理员使用这些表单。

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser


class CustomUserAdmin(UserAdmin):
    add_form = CustomUserCreationForm
    form = CustomUserChangeForm
    model = CustomUser
    list_display = ('email', 'is_staff', 'is_active',)
    list_filter = ('email', 'is_staff', 'is_active',)
    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        ('Permissions', {'fields': ('is_staff', 'is_active')}),
    )
    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('email', 'password1', 'password2', 'is_staff', 'is_active')}
        ),
    )
    search_fields = ('email',)
    ordering = ('email',)


admin.site.register(CustomUser, CustomUserAdmin)

这就是了。运行服务器并登录到管理站点。你应该能够像平常一样添加和改变用户。

image.png

总结

在这篇文章中,我们研究了如何创建一个自定义的用户模型,使电子邮件地址可以被用作主要的用户标识,而不是用于认证的用户名。

你可以在django-custom-user-modelrepo中找到两个选项的最终代码:AbstractUserAbstractBaseUser 。最后的代码示例还包括用户认证所需的模板、视图和URL。

想了解更多关于定制Django用户模型的信息?请看下面的资源。

  1. Options & Objects:定制Django用户模型
  2. 如何扩展Django用户模型
  3. 充分利用Django的用户模型(视频)
  4. 在Django中定制认证
  5. Django。如何扩展用户模型(又称自定义用户模型)