在Django中,使用电子邮件地址或用户名登录用户

43

我正在尝试创建一个认证后端,允许我的用户在Django 1.6中使用其电子邮件地址或用户名登录,使用自定义的用户模型。当我使用用户名登录时,该后端可以正常工作,但是使用电子邮件登录时却不能。我是否忘记了做某些事情?

from django.conf import settings
from django.contrib.auth.models import User

class EmailOrUsernameModelBackend(object):
    """
    This is a ModelBacked that allows authentication with either a username or an email address.

    """
    def authenticate(self, username=None, password=None):
        if '@' in username:
            kwargs = {'email': username}
        else:
            kwargs = {'username': username}
        try:
            user = User.objects.get(**kwargs)
            if user.check_password(password):
                return user
        except User.DoesNotExist:
            return None

    def get_user(self, username):
        try:
            return User.objects.get(pk=username)
        except User.DoesNotExist:
            return None

编辑:根据建议,我已经继承了ModelBackend并将其安装在我的设置中。 在我的设置中,我有以下内容: AUTHENTICATION_BACKENDS = ( 'users.backends', 'django.contrib.auth.backends.ModelBackend', ) 我已将后端更改为此:

from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
    """
    This is a ModelBacked that allows authentication with either a username or an email address.

    """
    def authenticate(self, username=None, password=None):
        if '@' in username:
            kwargs = {'email': username}
        else:
            kwargs = {'username': username}
        try:
            user = User.objects.get(**kwargs)
            if user.check_password(password):
                return user
        except User.DoesNotExist:
            return None

    def get_user(self, username):
        try:
            return User.objects.get(pk=username)
        except User.DoesNotExist:
            return None

现在我遇到了一个“users”模块没有定义“backends”属性/类的错误


2
我认为你需要继承自 django.contrib.auth.backends.ModelBackend(当然,还要在 AUTHENTICATION_BACKENDS 中安装它)。 - thebjorn
@thebjorn 我尝试从ModelBackend继承,但是我得到了一个不同的错误。我已经编辑了上面的问题。 - user3282276
我猜这是来自于AUTHENTICATION_BACKENDS = ( 'users.backends', 'django.contrib.auth.backends.ModelBackend')的第一个元素。它可能只需要是AUTHENTICATION_BACKENDS = ['yourapp.yourfile.EmailOrUsernameModelBackend'] - thebjorn
你可以使用 AUTHENTICATION_BACKENDS = ( 'users.backends', 'django.contrib.auth.backends.ModelBackend', ) 这个设置。这意味着它首先会查找 app: users,然后是一个名为 backends 的文件(我想)。但是你应该告诉 AUTHENTICATION_BACKENDS <app_name>.<file_name>.<class_name>。所以在你的例子中,应该是 'users.backends.EmailOrUsernameModelBackend' - Eagllus
我已经更改了AUTHENTICATION_BACKENDS的设置'users.backends.EmailOrUsernameModelBackend'。但它告诉我“Manager不可用;用户已被替换为'users.User'”。 - user3282276
所有这些关于ModelBackend的讨论,但没有链接到Django的默认实现来向我们展示它是什么? 让我们改变一下:Django ModelBackend源代码 - djvg
16个回答

33

另一个解决方案:

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


class EmailOrUsernameModelBackend(ModelBackend):
    """
    Authentication backend which allows users to authenticate using either their
    username or email address

    Source: https://dev59.com/5V8e5IYBdhLWcg3w2NNR#35836674
    """

    def authenticate(self, request, username=None, password=None, **kwargs):
        # n.b. Django <2.1 does not pass the `request`

        user_model = get_user_model()

        if username is None:
            username = kwargs.get(user_model.USERNAME_FIELD)

        # The `username` field is allows to contain `@` characters so
        # technically a given email address could be present in either field,
        # possibly even for different users, so we'll query for all matching
        # records and test each one.
        users = user_model._default_manager.filter(
            Q(**{user_model.USERNAME_FIELD: username}) | Q(email__iexact=username)
        )

        # Test whether any matched user has the provided password:
        for user in users:
            if user.check_password(password):
                return user
        if not users:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a non-existing user (see
            # https://code.djangoproject.com/ticket/20760)
            user_model().set_password(password)

修复:

  • 默认情况下,用户名字段中不禁止使用@符号,因此除非自定义用户模型禁止@符号,否则无法用其区分用户名和电子邮件。
  • 从技术上讲,可以有两个使用相同电子邮件的用户,一个在电子邮件字段中,另一个在用户名字段中。除非限制这种可能性,否则可能导致某个用户无法进行身份验证,或者当使用UserModel._default_manager.get(Q(username__iexact=username) | Q(email__iexact=username))时,未处理的MultipleObjectsReturned异常。
  • 通常使用except:捕获所有异常是不好的做法。

缺点 - 如果有两个用户,他们的电子邮件相同,并且密码也相同,则很容易认证第一个匹配项。我猜这种情况的机会非常小。

另外注意:任何一种方法都应该强制实施用户模型中唯一的email字段,因为默认的用户模型不定义唯一的电子邮件,这将导致在使用User.objects.get(email__iexact="...")时出现未处理的异常,或者认证第一个匹配项。无论哪种情况,使用电子邮件进行登录都假定电子邮件是唯一的。


1
如果有人看到这并遇到麻烦,请注意:Django 2.1在authenticate方法中需要 "request" 参数。因此,方法应该是 "def authenticate(self, request, username=None, password=None, **kwargs):"。如“Features Removed”中所述: https://docs.djangoproject.com/en/2.1/releases/2.1/。 - Josh Jobin
这是最优解。 - Valachio
最佳答案。使用这个方法完美解决了问题。再结合关于唯一电子邮件地址的这个答案,就更加完美了。 - Redgren Grumbholdt

21

在遵循上面给我的建议并更改 AUTHENTICATION_BACKENDS = ['yourapp.yourfile.EmailOrUsernameModelBackend'] 后,我得到了错误信息 Manager isn't available; User has been swapped for 'users.User'。这是因为我使用了默认的User模型,而不是自定义的模型。以下是可行的代码。

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

class EmailOrUsernameModelBackend(ModelBackend):
    """
    This is a ModelBacked that allows authentication
    with either a username or an email address.
    
    """
    def authenticate(self, username=None, password=None):
        if '@' in username:
            kwargs = {'email': username}
        else:
            kwargs = {'username': username}
        try:
            user = get_user_model().objects.get(**kwargs)
            if user.check_password(password):
                return user
        except User.DoesNotExist:
            return None

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

9
我想为其他遇到类似情况的人提供更简单的方法:

我认为,对于任何遇到这种情况的人,我的简化方法都非常有用:

# -*- coding: utf-8 -*-
from django.contrib.auth import backends, get_user_model
from django.db.models import Q


class ModelBackend(backends.ModelBackend):
    def authenticate(self, username=None, password=None, **kwargs):
        UserModel = get_user_model()

        try:
            user = UserModel.objects.get(Q(username__iexact=username) | Q(email__iexact=username))

            if user.check_password(password):
                return user
        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)

注意:

  • 忽略USERNAME_FIELD,尽管您可以很容易地将其添加回来
  • 不区分大小写(但是您可以删除__iexact以使其区分大小写)

6

我知道这个问题已经有答案了,但是我发现使用Django auth视图实现登录同时支持电子邮件和用户名的方法非常简洁。我没有看到任何人使用这种方法,所以我想分享一下。

from django.contrib.auth.models import User


class EmailAuthBackend():
    def authenticate(self, username=None, password=None):
        try:
            user = User.objects.get(email=username)
            if user.check_password(raw_password=password):
                return user
            return None
        except User.DoesNotExist:
            return None

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

然后在你的settings.py文件中添加以下内容:
AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
    'myapp.authentication.EmailAuthBackend',
)

6
我用两个简单的步骤编写了这段代码:
  1. VIEWS.py
     if request.method == 'POST':
            userinput = request.POST['username']

            try:
                username = userbase.objects.get(email=userinput).username
            except userbase.DoesNotExist:
                username = request.POST['username']
            password = request.POST['password']
  1. INDEX.html

    我创建了两个输入字段,第一个是用户名/电子邮件。我获取输入并尝试在数据库的电子邮件列中搜索相同的数据,如果匹配,则返回用户名并尝试进行身份验证,如果不匹配,则直接使用输入作为用户名。

我正在使用Django 2.2


5
您可以使用 Try 块来完成这个操作。 使用:
email = request.POST['username'] 
raw_password = request.POST['password'] 

   try:
        account = authenticate(username=MyUserAccount.objects.get(email=email).username,password=raw_password)
        if account is not None:
            login(request, account)
            return redirect('home')

    except:
        account = authenticate(username=email, password=raw_password)
        if account is not None:
            login(request, account)
            return redirect('home')

2

更新了同样的代码片段,增强了安全性。并且它允许你启用或禁用区分大小写的身份验证。如果您愿意,可以直接从pypi安装它

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

###################################
"""  DEFAULT SETTINGS + ALIAS   """
###################################


try:
    am = settings.AUTHENTICATION_METHOD
except:
    am = 'both'
try:
    cs = settings.AUTHENTICATION_CASE_SENSITIVE
except:
    cs = 'both'

#####################
"""   EXCEPTIONS  """
#####################


VALID_AM = ['username', 'email', 'both']
VALID_CS = ['username', 'email', 'both', 'none']

if (am not in VALID_AM):
    raise Exception("Invalid value for AUTHENTICATION_METHOD in project "
                    "settings. Use 'username','email', or 'both'.")

if (cs not in VALID_CS):
    raise Exception("Invalid value for AUTHENTICATION_CASE_SENSITIVE in project "
                    "settings. Use 'username','email', 'both' or 'none'.")

############################
"""  OVERRIDDEN METHODS  """
############################


class DualAuthentication(ModelBackend):
    """
    This is a ModelBacked that allows authentication
    with either a username or an email address.
    """

    def authenticate(self, username=None, password=None):
        UserModel = get_user_model()
        try:
            if ((am == 'email') or (am == 'both')):
                if ((cs == 'email') or cs == 'both'):
                    kwargs = {'email': username}
                else:
                    kwargs = {'email__iexact': username}

                user = UserModel.objects.get(**kwargs)
            else:
                raise
        except:
            if ((am == 'username') or (am == 'both')):
                if ((cs == 'username') or cs == 'both'):
                    kwargs = {'username': username}
                else:
                kwargs = {'username__iexact': username}

                user = UserModel.objects.get(**kwargs)
        finally:
            try:
                if user.check_password(password):
                    return user
            except:
                # Run the default password hasher once to reduce the timing
                # difference between an existing and a non-existing user.
                UserModel().set_password(password)
                return None

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

2

我尝试通过Django库了解他们如何验证管理员用户,我从Django库认证类中找到了一些代码,这段代码对我有用,我在此处附上。

在您的应用程序中创建一个yourFavNameAuthenticate.py文件,并将此代码粘贴到其中

yourFavNameAuthenticate.py

from django.db.models import Q

from django.contrib.auth import get_user_model

UserModel = get_user_model()


class UsernameOrEmailBackend(object):
    def authenticate(self, request, username=None, password=None, **kwargs):
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        if username is None or password is None:
            return
        try:
            user = UserModel.objects.get(
                Q(username=username) | Q(email=username))
        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

    def user_can_authenticate(self, user):
        """
        Reject users with is_active=False. Custom user models that don't have
        that attribute are allowed.
        """
        is_active = getattr(user, 'is_active', None)
        return is_active or is_active is None

    def get_user(self, user_id):
        try:
            user = UserModel._default_manager.get(pk=user_id)
        except UserModel.DoesNotExist:
            return None
        return user if self.user_can_authenticate(user) else None

前往 settings.py 并粘贴一行代码,告诉程序在哪里查找认证文件。

AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend', #<- This is the Django's authentication class object
    'appName.yourFavNameAuthenticate.UsernameOrEmailBackend', #<-This is yourFavNameAuthenticate.py class object
)

1
如果您正在使用django-rest-auth,则内置了使用电子邮件地址进行身份验证的选项,可能会与其他提出的方法冲突。 您只需要将以下内容添加到settings.py中即可:
ACCOUNT_AUTHENTICATION_METHOD = 'username_email'
ACCOUNT_EMAIL_REQUIRED = False
ACCOUNT_USERNAME_REQUIRED = False

#Following is added to enable registration with email instead of username
AUTHENTICATION_BACKENDS = (
 # Needed to login by username in Django admin, regardless of `allauth`
 "django.contrib.auth.backends.ModelBackend",

 # `allauth` specific authentication methods, such as login by e-mail
 "allauth.account.auth_backends.AuthenticationBackend",
)

Django rest auth email instead of username https://django-allauth.readthedocs.io/en/latest/configuration.html

请注意,除非您想要一个单一的框,用户可以在其中键入用户名或电子邮件地址,否则您需要在前端进行一些工作,以决定将登录请求发送为电子邮件、密码还是用户名、密码。我进行了一个简单的测试,检查用户输入是否包含'@'和'.'。我认为有人故意创建一个类似于电子邮件地址但不是他们的电子邮件地址的用户名的可能性很小,因此我不支持这种情况。

1
为了尽可能简化事情,您可以只需对ModelBackend进行快速检查,并在记录中匹配电子邮件时更改传递的用户名。通过这种方式,您可以最小化对ModelBackend的影响。
#myapp.auth.py
from django.contrib.auth.backends import ModelBackend, UserModel

class EmailThenUsernameModelBackend(ModelBackend):
    """
    Overwrites functionaility for ModelBackend related to authenticate function
    """
    def authenticate(self, request, username=None, password=None, **kwargs):
        """
        Check if passed username matches an email on record
            If so: override the email with the username on record
            Else: pass the username as is
        """
        user=UserModel._default_manager.filter(email=username).first()
        if user:
            username=user.username
        return super().authenticate(request, username, password, **kwargs)

# myapp.settings.py 
...
AUTHENTICATION_BACKENDS = ['myapp.auth.EmailThenUsernameModelBackend']

默认情况下,Django 不会强制电子邮件地址唯一。您可以将以下代码添加到自定义用户模型中实现该功能:
# myapp.models.py
class CustomUser(AbstractUser):
    # Overwrite email field at django.contrib.auth.models.AbstractUser
    # Force email field to exist and to be unique
    email = models.EmailField(
        _('email address'),
        unique=True
    )

注意:这不包括获取自定义用户设置所需的额外步骤。

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