如何用电子邮件字段完全取代Django认证中的用户名字段?
这篇文章逐步解释了如何在Django中创建一个自定义的用户模型,以便电子邮件地址可以被用作主要的用户标识符,而不是用于认证的用户名。
请记住,本帖所概述的过程需要对数据库模式进行重大修改。正因为如此,我们只建议新项目使用这个方法。如果这是一个现有的遗留项目,你可能需要备份数据并重新创建数据库。关于这个问题的更多信息,请查阅以下资源。
- Django官方文档中的自定义用户模型替代指南
- 迁移到Django的自定义用户模型博文
目标
在本文结束时,你应该能够。
- 描述
AbstractUser和 之间的区别。AbstractBaseUser - 解释为什么你在开始一个新的Django项目时应该设置一个自定义的用户模型
- 用自定义的用户模型启动一个新的Django项目
- 使用电子邮件地址作为主要的用户标识,而不是使用用户名进行认证
- 在实现自定义用户模型的过程中,练习测试优先的开发方式
AbstractUser vs AbstractBaseUser
Django的默认用户模型在认证时使用用户名来唯一地识别用户。如果你想使用电子邮件地址,你需要通过子类化AbstractUser 或AbstractBaseUser 来创建一个自定义的User模型。
选项。
AbstractUser:如果你对用户模型上现有的字段很满意,只是想删除用户名字段,请使用这个选项。AbstractBaseUser:如果你想通过创建你自己的、全新的用户模型从头开始,请使用这个选项。
我们将在这篇文章中讨论这两个选项,
AbstractUser和AbstractBaseUser。
每个选项的步骤都是一样的。
- 创建一个自定义的用户模型和管理器
- 更新settings.py
- 定制
UserCreationForm和UserChangeForm表格 - 更新管理员
强烈建议在开始一个新的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换成Poetry或Pipenv。更多信息,请查看现代Python环境。
不要应用迁移。记住:你必须在应用第一次迁移之前创建自定义的用户模型。
将新的应用程序添加到settings.py的INSTALLED_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)
决定你想使用哪个选项:subclassingAbstractUser 或AbstractBaseUser 。
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
在这里,我们
- 创建了一个名为
CustomUser的新类,该类的子类为AbstractUser - 删除了
username字段 - 使得
email字段成为必需的和唯一的 - 将
USERNAME_FIELD--它定义了User模型的唯一标识符--设置为email - 指定该类的所有对象来自于
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
在这里,我们
- 创建了一个名为
CustomUser的新类,该类的子类为AbstractBaseUser - 添加了
email,is_staff,is_active, 和 的字段。date_joined - 将
USERNAME_FIELD-- 定义了User模型的唯一标识符 -- 设置为email - 指定该类的所有对象来自于
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_name或last_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'...
表格
接下来,让我们对UserCreationForm 和UserChangeForm 表单进行子类化,使它们使用新的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)
这就是了。运行服务器并登录到管理站点。你应该能够像平常一样添加和改变用户。
总结
在这篇文章中,我们研究了如何创建一个自定义的用户模型,使电子邮件地址可以被用作主要的用户标识,而不是用于认证的用户名。
你可以在django-custom-user-modelrepo中找到两个选项的最终代码:AbstractUser 和AbstractBaseUser 。最后的代码示例还包括用户认证所需的模板、视图和URL。
想了解更多关于定制Django用户模型的信息?请看下面的资源。