Почему аутентификация в drf не работает и как мне заставить ее работать?

Я использую Django с rest framework. Регистрация работает нормально, но когда я вхожу в систему, после передачи адреса электронной почты и пароля серверной части, authenticate() возвращает None, даже если учетные данные верны. Я использую пользовательскую модель пользователя. Вот соответствующие строки кода:

models.py (не включает все поля)

class CustomUserManager(BaseUserManager):
    def create_user(self, email, password, **kwargs):
        if not email:
            raise ValueError('The user must enter a valid email.')
        email = self.normalize_email(email)
        user = self.model(email=email, **kwargs)
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, password, **kwargs):
        kwargs.setdefault('is_staff', True)
        kwargs.setdefault('is_superuser', True)
        kwargs.setdefault('is_active', True)

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

class CustomUser(AbstractBaseUser, PermissionsMixin):
    user_id = models.AutoField(primary_key=True)

    first_name = models.CharField(max_length=255, blank=True)
    middle_name = models.CharField(max_length=255, blank=True)
    last_name = models.CharField(max_length=255, blank=True)

    email = models.EmailField(unique=True)

    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    is_superuser = models.BooleanField(default=False)

    objects = CustomUserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['first_name', 'last_name']

    groups = models.ManyToManyField(
        "auth.Group",
        related_name="customuser_set",
        blank=True
    )
    user_permissions = models.ManyToManyField(
        "auth.Permission",
        related_name="customuser_set",
        blank=True
    )

viewsets.py

@action(detail=False, methods=["post"], permission_classes=[AllowAny])
    def login(self, request):
        email = request.data.get("email")
        password = request.data.get("password")
        user = CustomUser.objects.get(email=email)
        print(user)
        user = authenticate(request, email=email, password=password) # this returns None all the time
        if user:
            login(request, user)
            return Response({"message": "Login successful!"})
        return Response({"error": "Invalid credentials"}, status=401)

settings.py

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
]

Я не уверен, в чем проблема. Выполнение user = CustomUser.objects.get(email=email) также не приводит к появлению экземпляра CustomUser.

Внутри вашего приложения Django создайте файл authentication.py и добавьте:

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

User = get_user_model()

class EmailBackend(ModelBackend):
    """
    Custom authentication backend to authenticate users using their email and password.
    """
    def authenticate(self, request, email=None, password=None, **kwargs):
        try:
            user = User.objects.get(email=email)
        except User.DoesNotExist:
            return None

        if user.check_password(password):
            return user
        return None

Измените список AUTHENTICATION_BACKENDS в settings.py:

AUTHENTICATION_BACKENDS = [
    'your_app.authentication.EmailBackend',  # Custom authentication backend
    'django.contrib.auth.backends.ModelBackend',  # Default authentication backend
]

Измените метод входа в систему в вашем наборе представлений:

@action(detail=False, methods=["post"], permission_classes=[AllowAny])
def login(self, request):
    email = request.data.get("email")
    password = request.data.get("password")

    user = authenticate(request, email=email, password=password)  # Now should work

    if user:
        login(request, user)
        return Response({"message": "Login successful!"})
    
    return Response({"error": "Invalid credentials"}, status=401)


И теперь вы можете протестировать с помощью:

user = CustomUser.objects.get(email=email)
print(user.check_password(password))  # Should return True if the password is correct

Серверная часть аутентификации Django по умолчанию (ModelBackend) ожидает, что в качестве идентификатора будет использоваться имя пользователя. Однако, когда пользовательская модель пользователя определяет электронную почту как поле USERNAME_FIELD, Django не распознает ее во время аутентификации.

Чтобы исправить это, необходимо внедрить пользовательский сервер аутентификации, как справедливо подчеркнуто @berlin выше.

Шаг 1: Создайте пользовательский сервер аутентификации

Создайте новый файл в соответствующем приложении (например, users/auth_backend.py) и добавьте:

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

UserModel = get_user_model()

class EmailBackend(ModelBackend):
    """
    Custom authentication backend to authenticate users using email instead of username.
    """

    def authenticate(self, request, email=None, password=None, **kwargs):
        if email is None or password is None:
            return None

        try:
            user = UserModel.objects.get(email=email)
        except UserModel.DoesNotExist:
            return None

        if user.check_password(password) and self.user_can_authenticate(user):
            return user

        return None

Шаг 2: Зарегистрируйте пользовательский сервер аутентификации

Изменить settings.py чтобы указать Django использовать этот пользовательский сервер аутентификации:

AUTHENTICATION_BACKENDS = [
    'users.auth_backend.EmailBackend',  # Adjust path based on the project structure
    'django.contrib.auth.backends.ModelBackend',  # Keep Django's default
]

Зачем сохранять ModelBackend?

Если аутентификация Django по умолчанию (например, вход администратора с использованием имени пользователя) все еще требуется, рекомендуется использовать ModelBackend в качестве запасного варианта.

Шаг 3: Обновите функцию входа в систему

Измените представление, обрабатывающее аутентификацию, чтобы использовать новый сервер:

from django.contrib.auth import authenticate, login
from django.contrib.auth.models import update_last_login
from rest_framework.response import Response
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny

@action(detail=False, methods=["post"], permission_classes=[AllowAny])
def login(self, request):
    email = request.data.get("email")
    password = request.data.get("password")

    user = authenticate(request, email=email, password=password)  # Now works correctly
    if user:
        login(request, user)
        update_last_login(None, user)  # Updates last login timestamp
        return Response({"message": "Login successful!"})

    return Response({"error": "Invalid credentials"}, status=401)

Шаг 4: Перезапустите сервер Django

После внесения этих изменений перезапустите Django, чтобы убедиться, что применен новый сервер аутентификации:

python manage.py migrate
python manage.py runserver

Ожидаемый результат:

  • проверка подлинности (запрос, электронная почта=email, пароль=password) вернет экземпляр пользователя корректно.
  • Пользователи смогут без проблем войти в систему, используя свой адрес электронной почты и пароль.
  • Серверная часть проверки подлинности теперь будет работать должным образом для входа в систему по электронной почте.

Дополнительные советы по отладке

Если вход в систему по-прежнему не удается, проверьте следующее:

  1. Убедитесь, что выполнены миграции (python manage.py migrate)
  2. Подтвердите, что электронное письмо существует в базе данных
  3. Используйте print(user.check_password(пароль)) в серверной части для проверки хэширования пароля
  4. Очистить сеансы Django (python manage.py shell → из django.contrib.sessions.models импортировать сеанс; Session.objects.all().удалить())

Это решение было проверено с помощью искусственного интеллекта, но протестировано и доработано вручную.

Вернуться на верх