Доработки системы авторизации и регистрации

⏱ 35 мин · 🧩 заданий: 6

Что доработаем

У нашего сайта статей есть профили, но регистрация и вход пока примитивные. Сделаем:

  1. Регистрацию и вход на CBV, недоступные вошедшим.
  2. Подтверждение email: аккаунт активен только после перехода по ссылке.
  3. Смену и сброс пароля.
  4. Вход по email.

Формы

modules/system/forms.py:

from django.contrib.auth.forms import AuthenticationForm, UserCreationForm


class UserRegisterForm(UserCreationForm):
    class Meta(UserCreationForm.Meta):
        fields = UserCreationForm.Meta.fields + ("email", "first_name", "last_name")

    def clean_email(self):
        email = self.cleaned_data.get("email")
        if email and User.objects.filter(email=email).exists():
            raise forms.ValidationError("Такой email уже используется в системе")
        return email

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields:
            self.fields[field].widget.attrs.update({"class": "form-control", "autocomplete": "off"})


class UserLoginForm(AuthenticationForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["username"].label = "Логин или email"
        for field in self.fields:
            self.fields[field].widget.attrs.update({"class": "form-control", "autocomplete": "off"})

Регистрация и вход на CBV

from django.contrib.auth.views import LoginView, LogoutView
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.views.generic import CreateView

from modules.services.mixins import UserIsNotAuthenticated


class UserRegisterView(UserIsNotAuthenticated, CreateView):
    form_class = UserRegisterForm
    success_url = reverse_lazy("home")
    template_name = "system/registration/user_register.html"


class UserLoginView(UserIsNotAuthenticated, SuccessMessageMixin, LoginView):
    form_class = UserLoginForm
    template_name = "system/registration/user_login.html"
    next_page = "home"
    success_message = "Добро пожаловать на сайт!"


class UserLogoutView(LogoutView):
    next_page = "home"

success_url в атрибуте класса — через reverse_lazy: при импорте модуля маршруты ещё не загружены.

Подтверждение email

Новый пользователь создаётся неактивным, а на почту уходит ссылка с одноразовым токеном.

modules/services/email.py:

from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode


def send_activate_email_message(request, user):
    uid = urlsafe_base64_encode(force_bytes(user.pk))
    token = default_token_generator.make_token(user)
    activation_url = request.build_absolute_uri(
        reverse("confirm_email", kwargs={"uidb64": uid, "token": token})
    )
    subject = f"Активируйте свой аккаунт, {user.username}!"
    message = render_to_string("system/email/activate_email_send.html", {
        "user": user,
        "activation_url": activation_url,
        "site": get_current_site(request),
    })
    send_mail(subject, message, None, [user.email])
  • uidb64 — id пользователя в base64 (чтобы не светить число в адресе);
  • default_token_generator строит токен из id, хеша пароля, времени последнего входа и SECRET_KEY. После активации (или смены пароля) токен перестаёт работать. Срок жизни — PASSWORD_RESET_TIMEOUT (по умолчанию 3 дня).

Регистрация с отправкой письма:

class UserRegisterView(UserIsNotAuthenticated, CreateView):
    ...

    def form_valid(self, form):
        user = form.save(commit=False)
        user.is_active = False
        user.save()
        send_activate_email_message(self.request, user)
        messages.info(self.request, "Письмо со ссылкой для активации отправлено на ваш email")
        return redirect("home")

Подтверждение:

from django.contrib.auth import login
from django.utils.http import urlsafe_base64_decode
from django.views import View


class UserConfirmEmailView(View):
    def get(self, request, uidb64, token):
        try:
            uid = urlsafe_base64_decode(uidb64).decode()
            user = User.objects.get(pk=uid)
        except (TypeError, ValueError, OverflowError, User.DoesNotExist):
            user = None

        if user is not None and default_token_generator.check_token(user, token):
            user.is_active = True
            user.save()
            login(request, user)
            messages.success(request, "Ваш аккаунт активирован")
            return redirect("home")
        messages.error(request, "Ссылка недействительна или устарела")
        return redirect("home")

Маршруты:

urlpatterns = [
    path("register/", UserRegisterView.as_view(), name="register"),
    path("login/", UserLoginView.as_view(), name="login"),
    path("logout/", UserLogoutView.as_view(), name="logout"),
    path("confirm-email/<str:uidb64>/<str:token>/", UserConfirmEmailView.as_view(), name="confirm_email"),
]

Для разработки письма выводятся в консоль: EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend".

Смена и сброс пароля

Встроенные представления закрывают оба сценария — достаточно своих шаблонов и маршрутов:

from django.contrib.auth import views as auth_views

urlpatterns += [
    path("password-change/", auth_views.PasswordChangeView.as_view(
        template_name="system/registration/password_change.html",
        success_url=reverse_lazy("home"),
    ), name="password_change"),
    path("password-reset/", auth_views.PasswordResetView.as_view(
        template_name="system/registration/password_reset.html",
    ), name="password_reset"),
    path("password-reset/done/", auth_views.PasswordResetDoneView.as_view(
        template_name="system/registration/password_reset_done.html",
    ), name="password_reset_done"),
    path("reset/<uidb64>/<token>/", auth_views.PasswordResetConfirmView.as_view(
        template_name="system/registration/password_reset_confirm.html",
    ), name="password_reset_confirm"),
    path("reset/done/", auth_views.PasswordResetCompleteView.as_view(
        template_name="system/registration/password_reset_complete.html",
    ), name="password_reset_complete"),
]
Представление Шаг
PasswordChangeView Вошедший пользователь меняет пароль (нужен старый)
PasswordResetView Ввод email → письмо со ссылкой
PasswordResetDoneView «Письмо отправлено»
PasswordResetConfirmView Ввод нового пароля по ссылке
PasswordResetCompleteView «Пароль изменён»

Вход по email

Свой бэкенд аутентификации modules/system/backends.py:

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

User = get_user_model()


class UserModelBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        try:
            user = User.objects.get(Q(username=username) | Q(email__iexact=username))
        except (User.DoesNotExist, User.MultipleObjectsReturned):
            return None
        if user.check_password(password) and self.user_can_authenticate(user):
            return user
        return None
AUTHENTICATION_BACKENDS = ["modules.system.backends.UserModelBackend"]

user_can_authenticate() не пустит неактивного (не подтвердившего почту) пользователя.

Итог

  • Регистрация и вход — CreateView и LoginView с миксином UserIsNotAuthenticated.
  • Подтверждение email: неактивный пользователь + ссылка с uidb64 и токеном default_token_generator.
  • Смена и сброс пароля — пять встроенных представлений и свои шаблоны.
  • Вход по email — свой бэкенд в AUTHENTICATION_BACKENDS.

Практика решено 0 из 6

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Написать код

Напишите UserRegisterView(UserIsNotAuthenticated, CreateView): форма UserRegisterForm, шаблон system/registration/user_register.html, success_url = reverse_lazy("home").

Задание 2 Один вариант ответа

Почему в атрибуте класса success_url используют reverse_lazy, а не reverse?

Задание 3 Написать код

Сгенерируйте ссылку активации: закодируйте user.pk в base64 (urlsafe_base64_encode(force_bytes(user.pk))) в переменную uid и создайте токен default_token_generator.make_token(user) в переменную token.

Задание 4 Написать код

В представлении подтверждения проверьте токен: если user найден и default_token_generator.check_token(user, token) — сделайте user.is_active = True, сохраните пользователя и выполните вход login(request, user).

Задание 5 Несколько вариантов

Какие встроенные представления Django отвечают за сброс пароля по email?

Задание 6 Один вариант ответа

Что нужно, чтобы пользователи могли входить по email вместо логина?

Войдите, чтобы отмечать пройденные уроки.