Django - 使用电子邮件登录

130

我希望Django通过电子邮件进行用户身份验证,而不是通过用户名。一种方法是将电子邮件值提供为用户名值,但我不想这样做。原因是,我有一个URL/profile/<username>/,因此我不能有一个URL /profile/abcd@gmail.com/

另一个原因是所有电子邮件都是唯一的,但有时会出现用户名已被使用的情况。因此,我正在自动创建用户名为fullName_ID

我该如何让Django只使用电子邮件进行身份验证?

这是我创建用户的方式。

username = `abcd28`
user_email = `abcd@gmail.com`
user = User.objects.create_user(username, user_email, user_pass)

这是我登录的方式。

email = request.POST['email']
password = request.POST['password']
username = User.objects.get(email=email.lower()).username
user = authenticate(username=username, password=password)
login(request, user)

除了先获取用户名之外,还有其他登录方式吗?

18个回答

145

你应该编写一个自定义的身份验证后端。像这样的代码可以起到作用:

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend

class EmailBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        UserModel = get_user_model()
        try:
            user = UserModel.objects.get(email=username)
        except UserModel.DoesNotExist:
            return None
        else:
            if user.check_password(password):
                return user
        return None

然后,在设置中将该后端设置为您的认证后端:

AUTHENTICATION_BACKENDS = ['path.to.auth.module.EmailBackend']

更新。继承自ModelBackend,因为它已经实现了get_user()等方法。

在此处查看文档:https://docs.djangoproject.com/en/3.0/topics/auth/customizing/#writing-an-authentication-backend


6
使用django 1.9.8时,我遇到了一个错误:'EmailBackend'对象没有属性'get_user'。按照https://dev59.com/yWYr5IYBdhLWcg3wGmiq#13954358中的建议添加了'get_user'方法后问题得以解决。 - baltasvejas
2
请指定这段代码适用于哪个版本的Django。有些人抱怨get_user方法丢失了。 - Dr. Younes Henni
4
与其只使用if user.check_password(password):,你可能想要包括Django自带的默认操作ModelBackendif user.check_password(password) and self.user_can_authenticate(user):,以便检查用户是否具有is_active=True - jmq
5
如果没有在源代码中包含Django的缓解措施,那么这难道不容易受到时序攻击的影响吗? - Gabriel Garcia
6
根据我的回答,它容易受到定时攻击的影响;如果用户不存在,它不会检查密码,也就不会运行哈希算法,这将导致更快的响应。因此,如果攻击者得到一个比通常响应时间长的响应,意味着该用户存在(因为它必须检查密码,这涉及哈希函数)。我的回答是Django在他们的代码实现中的方式(根据我的回答),它将始终对密码进行哈希处理,混淆任何时间信息。 - Gabriel Garcia
显示剩余13条评论

85
如果您正在启动一个新项目,Django强烈建议您设置自定义用户模型。(参见https://docs.djangoproject.com/en/dev/topics/auth/customizing/#using-a-custom-user-model-when-starting-a-project)
如果您已经这样做了,请在用户模型中添加三行代码。
class MyUser(AbstractUser):
    USERNAME_FIELD = 'email'
    email = models.EmailField(_('email address'), unique=True) # changes email to unique and blank to false
    REQUIRED_FIELDS = [] # removes email from REQUIRED_FIELDS

当使用authenticate(email=email, password=password)时,它能够正常工作,而使用authenticate(username=username, password=password)时则停止工作。


8
运行createsuperuser时会出现错误:TypeError: create_superuser()缺少1个必需的位置参数:'username'。您需要使用自定义用户管理器:`class MyUserManager(BaseUserManager):def create_superuser(self, email, password, **kwargs): user = self.model(email=email, is_staff=True, is_superuser=True, **kwargs) user.set_password(password) user.save() return user` - Michal Holub
24
完整的指示在这里:https://www.fomfus.com/articles/how-to-use-email-as-username-for-django-authentication-removing-the-username - user2061057
3
然而,Django文档建议不要在创建可重用应用程序时使用自定义用户模型。 - djvg
1
这个解决方案不是很长吗? - Azhar Uddin Sheikh
@AzharUddinSheikh 在其他解决方案中,您最终会得到一个不合适的管理员部分。 - Saurabh Mahra

37

Django 3.x的电子邮件认证

如果要使用电子邮件/用户名和密码进行身份验证,而不是默认的用户名和密码身份验证,则需要覆盖ModelBackend类的两个方法:authenticate()和get_user():

get_user方法接受一个user_id参数,它可以是用户名、数据库ID或其他任何值,但必须对于您的用户对象是唯一的。它返回一个用户对象或None。如果您没有将电子邮件设置为唯一键,则必须处理查询集返回的多个结果。在下面的代码中,通过返回返回列表中的第一个用户来解决了这个问题。

from django.contrib.auth.backends import ModelBackend, UserModel
from django.db.models import Q

class EmailBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        try: #to allow authentication through phone number or any other field, modify the below statement
            user = UserModel.objects.get(Q(username__iexact=username) | Q(email__iexact=username))
        except UserModel.DoesNotExist:
            UserModel().set_password(password)
        except MultipleObjectsReturned:
            return User.objects.filter(email=username).order_by('id').first()
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user

    def get_user(self, user_id):
        try:
            user = UserModel.objects.get(pk=user_id)
        except UserModel.DoesNotExist:
            return None

        return user if self.user_can_authenticate(user) else None

默认情况下,AUTHENTICATION_BACKENDS设置为:

['django.contrib.auth.backends.ModelBackend']
在settings.py文件中,在底部添加以下内容以覆盖默认设置:
AUTHENTICATION_BACKENDS = ('appname.filename.EmailBackend',)

1
太好了,谢谢。我已经完成了一个项目,包括模板、表单、视图等等,所以重新开始并不那么吸引人!现在我可以通过电子邮件地址进行身份验证,有没有办法删除用户名字段,使其不包含在认证和模板中呢? - user4292309
2
为什么在模型中使用这个而不是USERNAME_FIELD变量? - Öykü
1
你可以使用电子邮件和用户名登录。 - George Nick Gorzynski
1
谢谢,你的解释非常好。 - boyenec

29

Django 4.0

您可以通过以下两种主要方式实现电子邮件验证,注意以下内容:

  • 为了减少拼写错误和恶意使用,用户模型上的电子邮件不应该是唯一的。
  • 仅当电子邮件地址已验证(即我们已发送验证电子邮件并且他们已单击验证链接)时,才应该将其用于身份验证。
  • 我们应该仅向已验证的电子邮件地址发送电子邮件。

自定义用户模型

在开始新项目时建议使用自定义用户模型,因为在项目中途更改可能会很麻烦。

我们将添加一个email_verified字段,以限制只有经过验证的电子邮件地址的用户才能进行电子邮件验证。

# app.models.py
from django.db import models
from django.contrib.auth.models import AbstractUser


class User(AbstractUser):
    email_verified = models.BooleanField(default=False)

我们将创建一个自定义身份验证后端,用于将给定的电子邮件地址替换为用户名。
此后端将与明确设置“电子邮件”字段以及设置“用户名”字段的身份验证表单一起使用。
# app.backends.py
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q

UserModel = get_user_model()


class CustomUserModelBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD, kwargs.get(UserModel.EMAIL_FIELD))
        if username is None or password is None:
            return
        try:
            user = UserModel._default_manager.get(
                Q(username__exact=username) | (Q(email__iexact=username) & Q(email_verified=True))
            )
        except UserModel.DoesNotExist:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a nonexistent user (#20760).
            UserModel().set_password(password)
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user


我们需要修改项目的settings.py文件,以使用我们自定义的用户模型和身份验证后端。
# project.settings.py

AUTH_USER_MODEL = "app.User"
AUTHENTICATION_BACKENDS = ["app.backends.CustomUserModelBackend"]

在运行migrate之前,请确保您运行了manage.py makemigrations,并且第一个迁移包含这些设置。

扩展用户模型

虽然比自定义User模型性能差(需要进行第二次查询),但在现有项目中extend现有的User模型可能更好,并且根据登录流程和验证过程可能更受欢迎。

我们通过AUTH_USER_MODEL设置从EmailVerification创建一个与我们项目正在使用的任何User模型的一对一关系。

# app.models.py
from django.conf import settings
from django.db import models


class EmailVerification(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_query_name="verification"
    )
    verified = models.BooleanField(default=False)

我们还可以创建一个自定义管理员,其中包含我们的扩展内联。

# app.admin.py
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin

from .models import EmailVerification

UserModel = get_user_model()


class VerificationInline(admin.StackedInline):
    model = EmailVerification
    can_delete = False
    verbose_name_plural = 'verification'


class UserAdmin(BaseUserAdmin):
    inlines = (VerificationInline,)


admin.site.unregister(UserModel)
admin.site.register(UserModel, UserAdmin)


我们接下来创建一个与上述类似的后端,仅检查相关模型的{{verified}}字段。
# app.backends.py
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q

UserModel = get_user_model()


class ExtendedUserModelBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD, kwargs.get(UserModel.EMAIL_FIELD))
        if username is None or password is None:
            return
        try:
            user = UserModel._default_manager.get(
                Q(username__exact=username) | (Q(email__iexact=username) & Q(verification__verified=True))
            )
        except UserModel.DoesNotExist:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a nonexistent user (#20760).
            UserModel().set_password(password)
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user


我们需要修改项目的settings.py文件,以使用我们的身份验证后端。
# project.settings.py

AUTHENTICATION_BACKENDS = ["app.backends.ExtendedUserModelBackend"]

你可以使用makemigrationsmigrate来为现有项目添加功能。
备注:
- 如果用户名不区分大小写,请将Q(username__exact=username)更改为Q(username__iexact=username)。 - 在生产中,防止新用户注册已经验证过的电子邮件地址。

谢谢你的好回答,但是 UserModel().set_password(password) 是什么意思? - tabebqena
2
我不理解你的评论,即电子邮件不应该是唯一的 - 拼写错误与任何事情有什么关系?看起来电子邮件应该是唯一的,否则你可能会出现两个不同的用户输入相同的电子邮件的情况。 - tadasajon
@tadasajon 无法保证用户拥有他们输入的电子邮件地址(可能会被恶意使用)。您可以使用约束和检查来防止两个用户共享相同的已验证电子邮件地址。详见https://docs.djangoproject.com/en/4.2/ref/models/constraints/。 - undefined
@tabebqena 对空用户对象上的密码进行哈希处理,以防止出现漏洞,通过请求完成所需的时间来确定用户账户是否存在:https://code.djangoproject.com/ticket/20760 - undefined

14

我有一个类似的需求,即在用户名字段中,用户名/电子邮件都应该有效。如果有人正在寻找身份验证后端的方法来实现此功能,请查看以下可行代码。如果需要仅使用电子邮件,请更改查询集。

from django.contrib.auth import get_user_model  # gets the user_model django  default or your own custom
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q


# Class to permit the athentication using email or username
class CustomBackend(ModelBackend):  # requires to define two functions authenticate and get_user

    def authenticate(self, username=None, password=None, **kwargs):
        UserModel = get_user_model()

        try:
            # below line gives query set,you can change the queryset as per your requirement
            user = UserModel.objects.filter(
                Q(username__iexact=username) |
                Q(email__iexact=username)
            ).distinct()

        except UserModel.DoesNotExist:
            return None

        if user.exists():
            ''' get the user object from the underlying query set,
            there will only be one object since username and email
            should be unique fields in your models.'''
            user_obj = user.first()
            if user_obj.check_password(password):
                return user_obj
            return None
        else:
            return None

    def get_user(self, user_id):
        UserModel = get_user_model()
        try:
            return UserModel.objects.get(pk=user_id)
        except UserModel.DoesNotExist:
            return None

同时在settings.py中添加AUTHENTICATION_BACKENDS = ( 'path.to.CustomBackend', )。


这个方法在我升级从1.11到2.1.5之前一直有效。你有什么想法为什么它不能在这个版本中工作? - zerohedge
@zerohedge 将请求添加到 authenticate 方法的参数中。请参阅 https://docs.djangoproject.com/en/2.2/topics/auth/customizing/#writing-an-authentication-backend - Van
它还会让您面临时序攻击的风险,因此值得尝试密切模仿Django实现以防止此类漏洞:https://github.com/django/django/blob/master/django/contrib/auth/backends.py - Gabriel Garcia
2
这也将使不活跃的用户能够进行身份验证。 - Gabriel Garcia

6

Django 2.X的电子邮件和用户名身份验证

考虑到这是一个常见问题,以下是一种自定义实现,模仿Django源代码,但可以使用用户名或电子邮件进行身份验证,不区分大小写,并保持时间攻击 保护不对非活动用户进行身份验证

from django.contrib.auth.backends import ModelBackend, UserModel
from django.db.models import Q

class CustomBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        try:
            user = UserModel.objects.get(Q(username__iexact=username) | Q(email__iexact=username))
        except UserModel.DoesNotExist:
            UserModel().set_password(password)
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user

    def get_user(self, user_id):
        try:
            user = UserModel.objects.get(pk=user_id)
        except UserModel.DoesNotExist:
            return None

        return user if self.user_can_authenticate(user) else None

始终记得在您的settings.py文件中添加正确的身份验证后端


1
我的理解是,UserModel().set_password(password) 的作用是防止攻击者通过执行大致相同数量的加密工作来确定用户是否存在(我假设这就是您所说的时序攻击)? - Victor
@GrandPhuba 你是100%正确的。 - Gabriel Garcia

5

似乎这个方法已经在Django 3.0中更新了。

对我有效的方法是:

authentication.py # <-- 我将其放在了一个应用程序中(在项目文件夹与settings.py并排放置无法工作)

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.hashers import check_password
from django.contrib.auth.models import User

class EmailBackend(BaseBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        UserModel = get_user_model()
        try:
            user = UserModel.objects.get(email=username)
        except UserModel.DoesNotExist:
            return None
        else:
            if user.check_password(password):
                return user
        return None

    def get_user(self, user_id):
        UserModel = get_user_model()
        try:
            return UserModel.objects.get(pk=user_id)
        except UserModel.DoesNotExist:
            return None

然后将此添加到settings.py文件中

AUTHENTICATION_BACKENDS = (
    'appname.authentication.EmailBackend',
)

4
我已经创建了一个助手:authenticate_user(email, password)函数,用于此目的。
from django.contrib.auth.models import User


def authenticate_user(email, password):
    try:
        user = User.objects.get(email=email)
    except User.DoesNotExist:
        return None
    else:
        if user.check_password(password):
            return user

    return None

class LoginView(View):
    template_name = 'myapp/login.html'

    def get(self, request):
        return render(request, self.template_name)

    def post(self, request):
        email = request.POST['email']
        password = request.POST['password']
        user = authenticate_user(email, password)
        context = {}

        if user is not None:
            if user.is_active:
                login(request, user)

                return redirect(self.request.GET.get('next', '/'))
            else:
                context['error_message'] = "user is not active"
        else:
            context['error_message'] = "email or password not correct"

        return render(request, self.template_name, context)

1
简单明了的解决方案!它会起作用,除非您在同一电子邮件下有多个用户。这是一个相当不错的实现。 - Richard Lucas

3
2023年7月更新:
你可以使用电子邮件密码来设置身份验证,而不是用户名密码。在这个说明中,用户名被移除了,并且我尽量不改变Django的默认设置。*你还可以查看我的答案,解释如何通过OneToOneField()扩展用户模型以添加额外的字段,以及查看我的答案我的答案,解释AbstractUserAbstractBaseUser之间的区别。
首先,运行下面的命令来创建account应用程序:
python manage.py startapp account

然后,将account应用程序设置为INSTALLED_APPS,并在settings.py中设置AUTH_USER_MODEL = 'account.User',如下所示:
# "settings.py"

INSTALLED_APPS = [
    ...
    "account", # Here
]

AUTH_USER_MODEL = 'account.User' # Here

然后,在account文件夹下创建managers.py,并在managers.py中创建一个继承(UM)UserManagerUserManager类,如下所示。*只需将下面的代码复制粘贴到managers.py中,并且managers.py是必要的,以使命令python manage.py createsuperuser正常工作,不会出现任何错误:
# "account/managers.py"

from django.contrib.auth.models import UserManager as UM
from django.contrib.auth.hashers import make_password

class UserManager(UM):
    def _create_user(self, email, password, **extra_fields):
        if not email:
            raise ValueError("The given email must be set")
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.password = make_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email=None, password=None, **extra_fields):
        extra_fields.setdefault("is_staff", False)
        extra_fields.setdefault("is_superuser", False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email=None, password=None, **extra_fields):
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("is_superuser", 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)

然后,创建一个扩展AbstractUserUser模型,并通过将其设置为None来移除username,并使用unique=True设置email,将email设置为USERNAME_FIELD,并在account/models.py中将UserManager设置为objects,如下所示。*只需将下面的代码复制并粘贴account/models.py中:
# "account/models.py"

from django.db import models
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import AbstractUser
from .managers import UserManager

class User(AbstractUser):
    username = None # Here
    email = models.EmailField(_("email address"), unique=True) # Here
    
    USERNAME_FIELD = 'email' # Here
    REQUIRED_FIELDS = []

    objects = UserManager() # Here

或者,您还可以创建一个扩展AbstractBaseUserPermissionsMixinUser模型,如下所示。*下面的代码与使用AbstractUser的代码等效:
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.utils import timezone
from .managers import UserManager

class User(AbstractBaseUser, PermissionsMixin):
    first_name = models.CharField(_("first name"), max_length=150, blank=True)
    last_name = models.CharField(_("last name"), max_length=150, blank=True)
    email = models.EmailField(_("email address"), unique=True)
    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)

    USERNAME_FIELD = 'email'

    objects = UserManager() # Here

    class Meta:
        verbose_name = _("user")
        verbose_name_plural = _("users")

*不要像下面这样扩展DefaultUser(User)模型,否则会出错:
# "account/models.py"

from django.contrib.auth.models import User as DefaultUser

class User(DefaultUser):
    ...

然后,在account/admin.py中创建一个继承UA(UserAdmin)UserAdmin类,如下所示。*只需将下面的代码复制并粘贴account/admin.py中:
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.admin import UserAdmin as UA
from .models import User

@admin.register(User)
class UserAdmin(UA):
    fieldsets = (
        (None, {"fields": ("password",)}),
        (_("Personal info"), {"fields": ("first_name", "last_name", "email")}),
        (
            _("Permissions"),
            {
                "fields": (
                    "is_active",
                    "is_staff",
                    "is_superuser",
                    "groups",
                    "user_permissions",
                ),
            },
        ),
        (_("Important dates"), {"fields": ("last_login", "date_joined")}),
    )
    add_fieldsets = (
        (
            None,
            {
                "classes": ("wide",),
                "fields": ("email", "password1", "password2"),
            },
        ),
    )
    list_display = ("email", "first_name", "last_name", "is_staff")
    ordering = ("-is_staff",)
    readonly_fields=('last_login', 'date_joined')

然后,运行以下命令。*在定制User模型时,这必须是对数据库的第一次迁移,否则根据我的实验和文档会出现错误,所以在开发Django项目之前,您必须先创建自定义的User模型:
python manage.py makemigrations && python manage.py migrate

然后,运行以下命令:
python manage.py createsuperuser

然后,运行以下命令:

python manage.py runserver 0.0.0.0:8000

然后,打开下面的网址:
http://localhost:8000/admin/login/

最后,您可以使用如下所示的电子邮件密码进行登录:

enter image description here

而且,这是如下所示的 自定义用户添加 页面:

enter image description here


3

使用电子邮件和用户名进行Django 2.x身份验证

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q

class EmailorUsernameModelBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        UserModel = get_user_model()
        try:
            user = UserModel.objects.get(Q(username__iexact=username) | Q(email__iexact=username))
        except UserModel.DoesNotExist:
            return None
        else:
            if user.check_password(password):
                return user
        return None

在settings.py中,添加以下行:
AUTHENTICATION_BACKENDS = ['appname.filename.EmailorUsernameModelBackend']

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接