Доработки системы авторизации и регистрации
Что доработаем
У нашего сайта статей есть профили, но регистрация и вход пока примитивные. Сделаем:
- Регистрацию и вход на CBV, недоступные вошедшим.
- Подтверждение email: аккаунт активен только после перехода по ссылке.
- Смену и сброс пароля.
- Вход по 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
Напишите UserRegisterView(UserIsNotAuthenticated, CreateView): форма UserRegisterForm,
шаблон system/registration/user_register.html, success_url = reverse_lazy("home").
class UserRegisterView(UserIsNotAuthenticated, CreateView):
form_class = UserRegisterForm
success_url = reverse_lazy("home")
template_name = "system/registration/user_register.html"
Почему в атрибуте класса success_url используют reverse_lazy, а не reverse?
Сгенерируйте ссылку активации: закодируйте user.pk в base64 (urlsafe_base64_encode(force_bytes(user.pk)))
в переменную uid и создайте токен default_token_generator.make_token(user) в переменную token.
from django.contrib.auth.tokens import default_token_generator
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = default_token_generator.make_token(user)
В представлении подтверждения проверьте токен: если user найден и
default_token_generator.check_token(user, token) — сделайте user.is_active = True, сохраните пользователя
и выполните вход login(request, user).
if user is not None and default_token_generator.check_token(user, token):
user.is_active = True
user.save()
login(request, user)
Какие встроенные представления Django отвечают за сброс пароля по email?
Что нужно, чтобы пользователи могли входить по email вместо логина?
Войдите, чтобы отмечать пройденные уроки.