本文的目的是讨论默认的Django用户模型实现的注意事项,同时也给你一些建议,如何解决这些问题。了解当前实现的局限性很重要,这样可以避免最常见的陷阱。
需要记住的是,Django用户模型在很大程度上是基于其至少16年前的初始实现。因为用户和认证是大多数使用Django的Web应用程序的核心部分,所以它的大部分怪癖在后续版本中持续存在,以保持向后兼容。
好消息是,Django提供了许多方法来覆盖和定制其默认实现,以适应你的应用需求。但其中一些改变必须在项目开始时就进行,否则在你的应用进入生产阶段后再改变数据库结构就太麻烦了。
用户模型的局限性
首先,让我们探讨一下注意事项,接下来我们讨论一下选项。
用户名字段是大小写敏感的
尽管username 字段被标记为唯一的,但在默认情况下它是不区分大小写的。这意味着用户名john.doe 和John.doe 在你的应用程序中识别两个不同的用户。
如果你的应用程序有社交方面的内容,围绕着username ,提供一个公共的URL到个人资料,例如Twitter、Instagram或GitHub,这可能是一个安全问题。
这也会带来糟糕的用户体验,因为人们不会想到john.doe 和John.Doe 是不同的用户名,如果用户在创建账户时没有完全按照相同的方式输入用户名,他们可能无法登录你的应用程序。
可能的解决方案
- 如果你使用的是PostgreSQL,你可以用
CICharField来代替用户名CharField(不区分大小写)。 - 你可以从
UserManager覆盖方法get_by_natural_key来查询数据库,使用iexact - 基于
ModelBackend实现创建一个自定义的认证后端
用户名字段对单码字母进行验证
这不一定是个问题,但你必须了解这意味着什么,有什么影响。
默认情况下,用户名字段接受字母、数字和字符。@,.,+,-, 和_ 。
这里的问题是它接受哪些字母。
例如,joão 将是一个有效的用户名。同样地,Джон 或約翰 也是一个有效的用户名。
Django提供了两个用户名验证器:ASCIIUsernameValidator 和UnicodeUsernameValidator 。如果你的目的是只接受A-Z的字母,你可能需要通过使用ASCIIUsernameValidator ,将用户名验证器切换为只使用ASCII字母。
可能的解决方案
- 替换默认的用户模型并将用户名验证器改为
ASCIIUsernameValidator - 如果你不能替换默认的用户模型,你可以在你用来创建/更新用户的表单上改变验证器
电子邮件字段是不唯一的
多个用户可以有相同的电子邮件地址与他们的账户关联。
默认情况下,电子邮件是用来恢复密码的。如果有一个以上的用户有相同的电子邮件地址,密码重置将为所有账户启动,用户将收到每个活动账户的电子邮件。
这也可能不是一个问题,但这肯定会使我们无法提供使用电子邮件地址来验证用户的选项(就像那些允许你用用户名或电子邮件地址登录的网站)。
可能的解决方案
- 使用
AbstractBaseUser替换默认的用户模型,从头开始定义电子邮件字段 - 如果你不能替换用户模型,在用于创建/更新的表单上强制执行验证。
电子邮件字段不是强制性的
默认情况下,电子邮件字段不允许null ,但它允许blank ,所以它几乎允许用户不告知电子邮件地址。
另外,这对你的应用程序来说可能不是一个问题。但如果你打算让用户用电子邮件登录,强制注册这个字段可能是个好主意。
当使用内置的资源,如用户创建表单或使用模型表单时,如果想要的行为是一直有用户的电子邮件,你需要注意这个细节。
可能的解决方案
- 使用
AbstractBaseUser替换默认的用户模型,从头定义电子邮件字段 - 如果你不能替换用户模型,在用于创建/更新的表单上强制验证
没有密码的用户不能启动密码重置
在用户创建过程中,有一个小小的陷阱,如果调用set_password 方法时传入None 作为参数,它将产生一个无法使用的密码。而这也意味着,用户将无法启动密码重置来设置第一个密码。
如果你使用Facebook或Twitter这样的社交网络来让用户在你的网站上创建一个账户,你就会落得这种情况。
另一种结束这种情况的方式是简单地使用User.objects.create_user() 或User.objects.create_superuser() ,而不提供初始密码来创建一个用户。
可能的解决方案
- 如果在你的用户创建流程中,你允许用户在不设置密码的情况下开始使用,记得要传递一个随机的(和冗长的)初始密码,这样用户以后可以通过密码重置流程来设置初始密码。
在你创建了初始迁移后,交换默认的用户模型是非常困难的
改变用户模型是你想在早期进行的事情。在你的数据库模式生成和你的数据库被填充之后,更换用户模型将是非常棘手的。
原因是你很可能会创建一些引用用户表的外键,同时Django内部表也会创建对用户表的硬引用。如果你打算以后改变这些,你就需要自己去改变和迁移数据库。
可能的解决方案
- 每当你开始一个新的Django项目时,一定要把默认的用户模型换掉。即使默认的实现符合你所有的需求。你可以简单地扩展
AbstractUser,改变设置模块上的一个配置。这将给你带来巨大的自由度,并且在将来需求发生变化时,会让事情变得更加简单。
详细的解决方案
为了解决我们在这篇文章中讨论的限制,我们有两个选择:(1)实施变通方法来修复默认用户模型的行为;(2)完全替换默认用户模型,并永久地修复这些问题。
决定你需要使用什么方法的是你的项目目前处于什么阶段。
- 如果你有一个在生产中运行的现有项目,并且正在使用默认的
django.contrib.auth.models.User,那么就采用第一个解决方案,实施解决方法。 - 如果你的Django项目刚刚开始,请从正确的角度出发,采用第二种解决方案。
变通方法
首先,让我们看看一些变通方案,如果你的项目已经投入生产,你可以实施这些方案。请记住,这些解决方案假定你不能直接访问用户模型,也就是说,你目前正在使用默认的用户模型,从django.contrib.auth.models 。
如果你确实替换了用户模型,那么请跳到下一节,以获得更好的修复问题的技巧。
使用户名字段不区分大小写
在做任何改变之前,你需要确保你的数据库中没有冲突的用户名。例如,如果你有一个用户名为maria 的用户和另一个用户名为Maria 的用户,你必须先计划进行数据迁移。很难告诉你该怎么做,因为这真的取决于你想怎么处理。一个选择是在用户名后面追加一些数字,但这可能会干扰用户体验。
现在我们假设你检查了你的数据库,没有冲突的用户名,你就可以开始了。
你需要做的第一件事是保护你的注册表格,不允许冲突的用户名来创建账户。
然后在你的用户创建表格上,用来注册,你可以像这样验证用户名:
def clean_username(self):
username = self.cleaned_data.get("username")
if User.objects.filter(username__iexact=username).exists():
self.add_error("username", "A user with this username already exists.")
return username
如果你使用DRF处理休息API中的用户创建,你可以在你的序列化器中做类似的事情:
def validate_username(self, value):
if User.objects.filter(username__iexact=value).exists():
raise serializers.ValidationError("A user with this username already exists.")
return value
在前面的例子中,提到的ValidationError 是DRF中定义的。
queryset参数上的iexact 符号会忽略这个情况而查询数据库。
现在,用户的创建已经被净化了,我们可以继续定义一个自定义的认证后端。
在你项目的任何地方创建一个名为backends.py的模块,并添加以下代码。
backends.py
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class CaseInsensitiveModelBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
UserModel = get_user_model()
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
try:
case_insensitive_username_field = '{}__iexact'.format(UserModel.USERNAME_FIELD)
user = UserModel._default_manager.get(**{case_insensitive_username_field: username})
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a non-existing user (#20760).
UserModel().set_password(password)
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user
现在在settings.py模块中切换认证后端。
settings.py
AUTHENTICATION_BACKENDS = ('mysite.core.backends.CaseInsensitiveModelBackend', )
请注意,'mysite.core.backends.CaseInsensitiveModelBackend' ,必须改为有效路径,即你创建backends.py模块的地方。
重要的是在改变认证后端之前已经处理了所有冲突的用户,因为否则它可能会引发一个500异常MultipleObjectsReturned 。
修复用户名验证,使其只使用接受ASCII字母
在这里我们可以借用内置的UsernameField ,并自定义它,将ASCIIUsernameValidator 添加到验证器列表中:
from django.contrib.auth.forms import UsernameField
from django.contrib.auth.validators import ASCIIUsernameValidator
class ASCIIUsernameField(UsernameField):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.validators.append(ASCIIUsernameValidator())
然后在你的用户创建表单的Meta ,你可以替换表单字段类:
class UserCreationForm(forms.ModelForm):
# field definitions...
class Meta:
model = User
fields = ("username",)
field_classes = {'username': ASCIIUsernameField}
修复电子邮件的唯一性并使其成为强制性的
在这里,你能做的就是在所有用户可以修改其电子邮件地址的视图中对用户输入进行消毒和处理。
你也必须在你的注册表单/序列化器中包含电子邮件字段。
然后就像这样把它变成强制性的:
class UserCreationForm(forms.ModelForm):
email = forms.EmailField(required=True)
# other field definitions...
class Meta:
model = User
fields = ("username",)
field_classes = {'username': ASCIIUsernameField}
def clean_email(self):
email = self.cleaned_data.get("email")
if User.objects.filter(email__iexact=email).exists():
self.add_error("email", _("A user with this email already exists."))
return email
你也可以在与本帖一起分享的项目中查看这个表单的完整和详细的例子。 用户工作法
替换默认的用户模型
现在我将向你展示我通常喜欢如何扩展和替换默认的用户模型。这有点啰嗦,但这是允许你访问用户模型的所有内部部分并使其更好的策略。
要替换用户模型,你有两个选择:扩展AbstractBaseUser 或扩展AbstractUser 。
为了说明这一点,我画了下面这个默认的Django模型的实现图:

标有User 的绿色圆圈实际上是你从django.contrib.auth.models 中导入的,这就是我们在这篇文章中讨论的实现。
如果你看一下源代码,它的实现是这样的:
class User(AbstractUser):
class Meta(AbstractUser.Meta):
swappable = 'AUTH_USER_MODEL'
所以基本上它只是一个AbstractUser 的实现。意味着所有的字段和逻辑都在抽象类中实现。
这样做是为了让我们可以通过创建一个AbstractUser 的子类来轻松地扩展User 模型,并添加你喜欢的其他功能和字段。
但是有一个限制,你不能覆盖一个现有的模型字段。例如,你可以重新定义电子邮件字段,使其成为强制性的或改变其长度。
因此,扩展AbstractUser 类只有在你想修改它的方法、添加更多的字段或交换objects 管理器时才有用。
如果你想删除一个字段或改变字段的定义方式,你必须从AbstractBaseUser 中扩展用户模型。
要完全控制用户模型,最好的策略是从PermissionsMixin和AbstractBaseUser 创建一个新的具体类。
请注意,只有当你打算使用Django admin或内置的权限框架时,PermissionsMixin 才是必要的。如果你不打算使用它,你可以不写它。如果将来情况有变,你可以添加mixin并迁移模型,就可以开始使用了。
所以实施策略是这样的:

现在我将向你展示我常用的实现方法。我总是使用PostgreSQL,在我看来,它是与Django一起使用的最佳数据库。至少它是拥有最多支持和功能的数据库。所以我将展示一种使用PostgreSQL的方法CITextExtension 。然后我将展示如果你使用其他数据库引擎的一些选择。
对于这个实现,我总是创建一个名为accounts 的应用程序:
django-admin startapp accounts
然后在添加任何代码之前,我喜欢创建一个空的迁移来安装我们要使用的PostgreSQL扩展:
python manage.py makemigrations accounts --empty --name="postgres_extensions"
在accounts 应用程序的migrations 目录中,你会发现一个名为0001_postgres_extensions.py 的空迁移。
修改该文件以包括扩展的安装。
migrations/0001_postgres_extensions.py
from django.contrib.postgres.operations import CITextExtension
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
]
operations = [
CITextExtension()
]
现在让我们来实现我们的模型。在accounts 应用程序中打开models.py 文件。
我总是直接从GitHub上的Django源码中抓取初始代码,复制AbstractUser 实现,并进行相应修改。
accounts/models.py
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin, UserManager
from django.contrib.auth.validators import ASCIIUsernameValidator
from django.contrib.postgres.fields import CICharField, CIEmailField
from django.core.mail import send_mail
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
class CustomUser(AbstractBaseUser, PermissionsMixin):
username_validator = ASCIIUsernameValidator()
username = CICharField(
_("username"),
max_length=150,
unique=True,
help_text=_("Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."),
validators=[username_validator],
error_messages={
"unique": _("A user with that username already exists."),
},
)
first_name = models.CharField(_("first name"), max_length=150, blank=True)
last_name = models.CharField(_("last name"), max_length=150, blank=True)
email = CIEmailField(
_("email address"),
unique=True,
error_messages={
"unique": _("A user with that email address already exists."),
},
)
is_staff = models.BooleanField(
_("staff status"),
default=False,
help_text=_("Designates whether the user can log into this admin site."),
)
is_active = models.BooleanField(
_("active"),
default=True,
help_text=_(
"Designates whether this user should be treated as active. Unselect this instead of deleting accounts."
),
)
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
objects = UserManager()
EMAIL_FIELD = "email"
USERNAME_FIELD = "username"
REQUIRED_FIELDS = ["email"]
class Meta:
verbose_name = _("user")
verbose_name_plural = _("users")
def clean(self):
super().clean()
self.email = self.__class__.objects.normalize_email(self.email)
def get_full_name(self):
"""
Return the first_name plus the last_name, with a space in between.
"""
full_name = "%s %s" % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
"""Return the short name for the user."""
return self.first_name
def email_user(self, subject, message, from_email=None, **kwargs):
"""Send an email to this user."""
send_mail(subject, message, from_email, [self.email], **kwargs)
让我们回顾一下我们在这里改变了什么:
- 我们把
username_validator改为使用ASCIIUsernameValidator username字段现在使用CICharField,它不区分大小写email字段现在是强制性的,唯一的,并使用不区分大小写的CIEmailField。
在设置模块中,添加以下配置:
settings.py
AUTH_USER_MODEL = "accounts.CustomUser"
现在,我们已经准备好创建我们的迁移了。
python manage.py makemigrations
应用迁移:
python manage.py migrate
如果你只是创建你的项目,并且没有其他的模型/应用程序,你应该得到一个类似的结果:
Operations to perform:
Apply all migrations: accounts, admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0001_initial... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
如果你检查你的数据库方案,你会发现没有auth_user 表(这是默认的),而现在用户被存储在accounts_customuser :

而所有指向用户模型的外键将被创建,并指向这个表。这就是为什么在你的项目开始时,在你创建数据库方案之前,就应该做这件事的原因。
现在你有了所有的自由。你可以替换掉first_name 和last_name ,只使用一个叫做name 的字段。你可以删除username 字段,用email 来标识你的用户模型(然后确保你把属性USERNAME_FIELD 改为email )。
你可以在GitHub上抓取源代码:customuser
在没有PostgreSQL的情况下处理不区分大小写的问题
如果你不使用PostgreSQL,想实现不区分大小写的认证,并且你可以直接访问用户模型,一个很好的黑客方法是为用户模型创建一个自定义管理器,像这样。
accounts/models.py
from django.contrib.auth.models import AbstractUser, UserManager
class CustomUserManager(UserManager):
def get_by_natural_key(self, username):
case_insensitive_username_field = '{}__iexact'.format(self.model.USERNAME_FIELD)
return self.get(**{case_insensitive_username_field: username})
class CustomUser(AbstractBaseUser, PermissionsMixin):
# all the fields, etc...
objects = CustomUserManager()
# meta, methods, etc...
然后你也可以在clean() 方法上对用户名字段进行消毒,使其总是保存为小写,这样你就不必为大小写变化/冲突的用户名而烦恼。
def clean(self):
super().clean()
self.email = self.__class__.objects.normalize_email(self.email)
self.username = self.username.lower()
结论
在本教程中,我们讨论了默认用户模型实现的一些注意事项,并提出了一些解决这些问题的方案。
这里的启示是:一定要替换默认的用户模型。
如果你的项目已经在生产中了,不要惊慌:有办法按照本帖的建议来解决这些问题。