воскресенье, 13 марта 2016 г.

Вход в систему по полям username или email

В предыдущей статье я подробно описал как можно заменить дефолтную модель пользователя django.contrib.auth.models.User своей собственной. Делали мы это для того чтобы изменить встроенное (из коробки) поведение фреймворка.

В этой статье, я хочу описать процесс реализации варианта использования, когда вход в систему пользователь может осуществить по одному из полей username, или email. Делается это очень просто — определением кастомного бэкенда аутентификации.

Следует отметить, что такого рода аутентификация возможна только в том случае, если username или email однозначно идентифицируют одного пользователя системы. Дефолтное (из коробки) поведение Django не разрешает этого добится, поскольку, в модели django.contrib.auth.models.User поле email не объявленно уникальным. Для реализации такой аутентификации, сперва необходимо заменить модель пользователя. Как это сделать описано в предыдущей статье.

Итак, допустим, мы имеем в проекте приложение под названием security. В нем создадим файл auth_backends.py в котором мы разместим наш бэкенд UsernameEmailAuthenticateBackend:

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


class UsernameEmailAuthenticateBackend(ModelBackend):
    def __init__(self):
        self.UserModel = get_user_model()

    def authenticate(self, username=None, password=None, **kwargs):
        try:
            user = self.UserModel.objects.get(Q(username=username) | Q(email=username))
            if password is not None:
                if user.check_password(password):
                    return user
            else:
                return user
        except self.UserModel.DoesNotExist:
            return None

    def get_user(self, user_id):
        try:
            return self.UserModel.objects.get(pk=user_id)
        except self.UserModel.DoesNotExist:
            return None
Как только мы реализуем этот бэкенд, настроим проект на его использование. Для этого, в файле settings.py определим кортеж AUTHENTICATION_BACKENDS таким образом:
...
AUTHENTICATION_BACKENDS = (
    'security.auth_backends.UsernameEmailAuthenticateBackend',
)
...

Это все. Теперь, Django будет использовать наш бэкенд для аутентификации пользователя.

вторник, 8 марта 2016 г.

Замена дефолтной модели User

Каким бы замечательным и продуманным ни был фреймворк, какие бы инструменты он не предоставлял из коробки, рано или поздно приходится вмешатся в его архитектуру и внести кое какие изменения. В этом смысле, Django, далеко не исключение. Вот небольшой пример. Допустим, нам нужно реализовать аутентификацию не по дефолтному полю username, а, скажем, по полю email. Для этого, нам уже потребуется определить свой класс пользователя и заменить им дефолтный класс django.contrib.auth.models.User.

В этой статье я подробно опишу как это сделать. Для примера, рассмотрим все тот же вариант использования, когда нужна аутентификация по полю email. Весь код класса я решил разбить на четыре части для удобства читаемости. Ничего особенного в этом классе нет. За исключением нескольких измененных строк, он один в один идентичен классу django.contrib.auth.models.AbstractUser. Вот как будет выглядеть реализация этого класса:

from django.db import models
from django.contrib.auth.models import UserManager
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.utils.translation import ugettext_lazy as _
from django.core import validators
from django.utils import timezone
from django.core.mail import send_mail

Создаваемый нами класс должен наследоватся от AbstractBaseUser и PermissionsMixin. Также, необходимо внести изменения в определения полей email и username:

class BaseCustomUser(AbstractBaseUser, PermissionsMixin):
    """
    An abstract base class implementing a fully featured User model with
    admin-compliant permissions.

    Username, password and email are required. Other fields are optional.
    """
    email = models.EmailField(
        _('Email Address'), unique=True,
        error_messages={
            'unique': _("A user with that email already exists."),
        }
    )
    username = models.CharField(_('username'), max_length=30, unique=True, blank=True, null=True,
        help_text=_('Required. 30 characters or fewer. Letters, digits and '
                    '@/./+/-/_ only.'),
        validators=[
            validators.RegexValidator(r'^[\w.@+-]+$',
                                      _('Enter a valid username. '
                                        'This value may contain only letters, numbers '
                                        'and @/./+/-/_ characters.'), 'invalid'),
        ],
        error_messages={
            'unique': _("A user with that username already exists."),
        }
    )

Обязательно вносим изменения в поля USERNAME_FIELD и REQUIRED_FIELDS, как показано ниже:

    first_name = models.CharField(_('first name'), max_length=30, blank=True)
    last_name = models.CharField(_('last name'), max_length=30, blank=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)

    objects = UserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

В остальном коде, в классе Meta, изменим значение поля abstract на False:

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')
        abstract = False

    def get_full_name(self):
        """
        Returns the first_name plus the last_name, with a space in between.
        """
        full_name = '{0} {1}'.format(self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        "Returns the short name for the user."
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        """
        Sends an email to this User.
        """
        send_mail(subject, message, from_email, [self.email], **kwargs)

Итак, для большей уверенности, что я делаю все правильно, я просто копирую весь код класса django.contrib.auth.models.AbstractUser и вношу нужные изменения. Конечно же, первое что приходит на ум, когда мы копируем код какого либо класса: "А не лучше будет унаследоватся от этого класса и переопределить нужные поля и методы?". К сожалению, согласно замечанию:

In normal Python class inheritance, it is permissible for a child class to override any attribute from the parent class. In Django, this is not permitted for attributes that are Field instances (at least, not at the moment). If a base class has a field called author, you cannot create another model field called author in any class that inherits from that base class.

на данный момент, невозможно унаследоватся от модели и переопределить поля или методы.

Как только мы создали свою модель, мы готовы подключить её в систему. Для этого, в файле settings.py, указываем Django использовать именно наш класс:

...
AUTH_USER_MODEL = 'security.CustomUser'
...

Теперь, вход в систему и регистрация будет осуществлятся исключительно по полю email вместо дефолтного поля username.

Существует возможность реализовать вариант использования, когда вход в систему производится по полю email или username. Как это сделать читайте в следующей статье.

Ярлыки

Популярные записи