Django 39. Капча и подтверждение регистрации по email
В этом посте подключим к сайту Google reCAPTCHA для защиты от ботов и реализуем подтверждение регистрации по электронной почте.
Дополнительные материалы
Для скачивания материалов необходимо войти или зарегистрироваться
Файлы также можно получить в Telegram-боте по коду: 256843
Реклама
Когда мы с вами работали над формой регистрации, мы упустили одну важную деталь – ботов. Если ничего не предпринимать, боты будут регистрироваться на сайте в большом количестве, нагружая базу данных заведомо неактуальной информацией, плодя спам в комментариях под постами и доставляя немало других мелких и не очень неприятностей.
Для решения этой проблемы мы сделаем следующее:
- Добавим на страницу регистрации Google reCAPTCHA, для явной защиты от ботов и массовых регистраций. Да, это не самая лучшая защита от ботов, но 99% всё же будут отсеяны.
- Добавим обязательное подтверждение регистрации по email. Этим мы отсеем оставшихся ботов.
Google reCAPTCHA.
Для того, чтобы у нас был доступ к сервису reCAPTCHA, необходимо получить ключи доступа.
Откроем сайт и авторизуемся: https://www.google.com/recaptcha/admin/
Выбираем "Создать" и заполняем три поля:
- Ярлык - вводим название сайта.
- Тип reCAPTCHA - выбираем "С помощью заданий (v2)".
- Домены - вводим домен без
https
илиwww
.
Нажимаем кнопку "Отправить". Откроется страница с ключами доступа, не закрывайте её, далее они нам понадобятся.
Django reCAPTCHA.
Для добавления капчи на страницу регистрации мы воспользуемся готовой библиотекой Django reCAPTCHA
.
Установим библиотеку, выполнив команду:
pip install django-recaptcha
Также добавим django-recaptcha4.0.0
в requirements.txt
.
Откроем файл settings.py
и добавим django_recaptcha
в INSTALLED_APPS
.
Там же создадим два параметра настроек:
RECAPTCHA_PUBLIC_KEY
- в этом параметре указываем первый полученный на сайте ключ "Ключ сайта".RECAPTCHA_PRIVATE_KEY
- в этом параметре указываем второй полученный на сайте ключ "Секретный ключ".
RECAPTCHA_PUBLIC_KEY = ""
RECAPTCHA_PRIVATE_KEY = ""
Добавляем поле в форму.
Откроем файл forms.py
в директории приложения user_app
и найдём наш класс RegistrationForm
.
Добавим в него всего одно поле:
from django_recaptcha.fields import ReCaptchaField
captcha = ReCaptchaField()
Этого достаточно. Теперь, открыв страницу регистрации, мы увидим новое поле с капчей.
Если вам нужны расширенные настройки, то документация к библиотеке доступна по ссылке: https://github.com/django-recaptcha/django-recaptcha
Подтверждение регистрации по email.
Для реализации отправки письма с подтверждением регистрации нам потребуется:
- Написать класс отправки письма. Это же будет заготовкой для внедрения фоновой отправки писем используя Celery.
- Изменить наш класс представления для регистрации и добавить два новых.
- Сделать три простых шаблона.
Обратите внимание, что для работы отправки почты необходимо прописать почтовый сервер в файле settings.py
. Подробнее об этом можно прочитать в посте "Django 12. Настройка отправки почты"
Класс отправки почты.
Начнём с класса, реализующего методы для отправки почты. Это также будет являться заготовкой для внедрения Celery, позволяющего выполнять задачи в фоне, но об этом в другой раз.
В директории приложения user_app
создадим новый файл tasks.py
.
Создадим класс SendEmail
.
Первым методом будет конструктор класса __init__
, принимающий аргумент user
.
В нём будет четыре поля:
self.user
- в него помещаем переданный в конструктор аргумент user. Это объект пользователя.self.current_site
- в него из классаSites
получаем домен сайта.self.token
- используя функциюdefault_token_generator
и методmake_token
, на основе пользователя создаём токен.self.uid
- в этом поле мы кодируем числовое значениеid
пользователя вbase64
.
class SendEmail:
def __init__(self, user: User):
self.user = user
self.current_site = Site.objects.get_current().domain
self.token = default_token_generator.make_token(self.user)
self.uid = urlsafe_base64_encode(str(self.user.pk).encode())
Вторым методом будет send_activate_email
.
В нём создадим три переменные:
reset_password_url
- в ней, используя функциюreverse_lazy
, получаем путь для активации, передаваяuid
иtoken
.subject
- в ней создаём строку с текстом для темы письма.message
- в ней создаём строку с телом письма. Позволяется использоватьHTML-разметку
, также используя функциюrender_to_string
, можно создатьHTML-файл
с шаблоном письма и отправлять его.
Ниже, обращаясь ко встроенному в класс пользователя методу email_user
, отправляем ему письмо, которое передаёт в него тему и сообщение.
def send_activate_email(self):
reset_password_url = reverse_lazy(
"user_app:signup_confirm", kwargs={"uidb64": self.uid, "token": self.token}
)
subject = f"Активация аккаунта на сайте {self.current_site}"
message = (
f"Благодарим за регистрацию на сайте {self.current_site}.\n"
"Для активации учётной записи, пожалуйста перейдите по ссылке:\n"
f"https://{self.current_site}{reset_password_url}\n"
)
self.user.email_user(subject=subject, message=message)
Полный код класса:
from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import Site
from django.urls import reverse_lazy
from django.utils.http import urlsafe_base64_encode
class SendEmail:
def __init__(self, user: User):
self.user = user
self.current_site = Site.objects.get_current().domain
self.token = default_token_generator.make_token(self.user)
self.uid = urlsafe_base64_encode(str(self.user.pk).encode())
def send_activate_email(self):
reset_password_url = reverse_lazy(
"user_app:signup_confirm", kwargs={"uidb64": self.uid, "token": self.token}
)
subject = f"Активация аккаунта на сайте {self.current_site}"
message = (
f"Благодарим за регистрацию на сайте {self.current_site}.\n"
"Для активации учётной записи, пожалуйста перейдите по ссылке:\n"
f"https://{self.current_site}{reset_password_url}\n"
)
self.user.email_user(subject=subject, message=message)
Под классом создаём функцию activate_email_task
, принимающую пользователя в аргументах.
Именно эту функцию будем вызывать из представления для отправки почты, а в будущем она будет выполняться как задача Celery.
В функции в переменную send_email
помещаем экземпляр класса SendEmail
.
Следующей строчкой вызываем метод send_activate_email
.
def activate_email_task(user: User):
send_email = SendEmail(user=user)
send_email.send_activate_email()
Изменение представления регистрации.
Откроем файл views.py
в директории приложения user_app
и найдём наш класс для регистрации CustomRegistrationView
.
В нём нам необходимо изменить метод form_valid
.
Изначально он был такой:
def form_valid(self, form):
form.save()
return super().form_valid(form)
В этом методе, сохраняем зарегистрированного пользователя.
Теперь же нам необходимо его доработать, а именно:
- В модели пользователя есть поле
is_active
, определяющее активирован/активен пользователь или нет. По умолчанию он установлен вTrue
, нам необходимо после регистрации присваиватьFalse
. - Вызвать функцию отправки почты, передав в неё объект зарегистрированного пользователя.
И тут проявляется забавная особенность. Если оставить return super().form_valid(form)
в конце метода, то активация работать не будет. Оказывается, генерация токена берёт за основу время изменения пользователя и получается следующая последовательность:
- Регистрация пользователя - первое время изменения
- Деактивация пользователя и сохранение - второе время изменения
- Отправка письма с токеном на основе времени изменения из пункта 2.
- Вызов
super().form_valid(form)
, который в свою очередь тоже сохраняет объект - третье время изменения.
Проще говоря, пользователю приходит невалидная ссылка для активации аккаунта. Это решается изменением последнего шага на return HttpResponseRedirect(self.get_success_url())
.
Код метода:
def form_valid(self, form):
user: User = form.save()
user.is_active = False
user.save()
activate_email_task(user)
return HttpResponseRedirect(self.get_success_url())
Также необходимо изменить метод get_success_url
, а именно изменить маршрут на страницу после регистрации с сообщением об отправке почты.
def get_success_url(self):
return reverse_lazy("user_app:signup_done")
Представление страницы активации и завершения регистрации.
Сделаем два представления:
- На первое будем перенаправлять после регистрации. Оно будет просто выводить страницу с текстом, не более.
- На второе пользователь будет попадать перейдя по ссылке из письма. В нём будет логика валидации токена и активации пользователя.
Начнём с первого представления. Создадим класс CustomRegistrationDoneView
, унаследованный от TemplateView
.
В нём пропишем два поля:
template_name
- файл шаблона страницы.extra_context
- заголовок страницы
Код представления:
class CustomRegistrationDoneView(TemplateView):
template_name = "user_app/signup_done.html"
extra_context = {"title": "Регистрация завершена, активируйте учётную запись."}
Второе представление будет объёмнее. Создадим класс CustomRegistrationConfirmView
, унаследованный от базового класса View
.
В классе создаём метод get
, принимающий в агрументах request
, uidb64
, token
.
В начале идёт блок try-except
, в котором мы декодируем uidb64
в int
и получаем соответствующего пользователя в переменную user
.
Если декодировать не удастся или пользователь с таким id
не будет найден, то поднимется исключение, в обработке которого в переменную user
присваиваем None
.
Далее блок if-else
. В условии проверяем, что user
не является None
и, что токен соответствует пользователю.
Если условие выполняется:
- Активируем пользователя, установив значение
is_active
, равноеTrue
. - Сохраняем пользователя.
- Используя функцию
login
, авторизуем пользователя, передавая в функциюrequest
иuser
. - Возвращаем работу функции
render
, передав в неёrequest
, путь до шаблона, уведомляющего об успешной активации, и контекст, содержащий заголовок страницы.
Если условие не было выполнено:
- Возвращаем работу функции
render
, передав в неёrequest
, путь до шаблона, уведомляющего об ошибке активации, и контекст, содержащий заголовок страницы.
Код представления:
class CustomRegistrationConfirmView(View):
def get(self, request, uidb64, token):
try:
uid = urlsafe_base64_decode(uidb64)
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)
return render(
request,
"user_app/signup_confirmed.html",
{"title": "Учётная запись активирована."},
)
else:
return render(
request,
"user_app/signup_not_confirmed.html",
{"title": "Ошибка активации учётной записи."},
)
URL-маршруты.
Добавим для наших представлений соответствующие им маршруты в файл urls.py
:
path("signup/", views.CustomRegistrationView.as_view(), name="signup"),
path(
"signup_done/", views.CustomRegistrationDoneView.as_view(), name="signup_done"
),
path(
"signup_confirm/<uidb64>/<token>/",
views.CustomRegistrationConfirmView.as_view(),
name="signup_confirm",
),
Шаблоны представлений.
У нас два представления, использующих три шаблона. Откройте директорию с шаблонами приложения user_app
и создайте три файла:
signup_done.html
- страница с уведомлением, что письмо было отправлено.signup_confirmed.html
- страница успешного подтверждения почты.signup_not_confirmed.html
- страницы ошибки при подтверждении.
signup_done.html
{% extends "blog/base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container mt-3 justify-content-center d-flex">
<div class="col-lg-4 col-sm-12">
<h2>Регистрация завершена.</h2>
<p>Для активации учётной записи необходимо подтвердить вашу электронную почту.</p>
<p>Письмо со ссылкой для активации уже отправлено.</p>
</div>
</div>
{% endblock %}
signup_confirmed.html
{% extends "blog/base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container mt-3 justify-content-center d-flex">
<div class="col-lg-4 col-sm-12">
<h2>Учётная запись активирована.</h2>
<p>Ваша электронная почта была подтверждена успешно.</p>
<a href="{% url "user_app:user_profile" username=user.username %}" class="btn btn-primary my-btn mb-3">
Перейти в личный кабинет
</a>
</div>
</div>
{% endblock %}
signup_not_confirmed.html
{% extends "blog/base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="container mt-3 justify-content-center d-flex">
<div class="col-lg-4 col-sm-12">
<h2>Ошибка активации учётной записи.</h2>
<p>Произошла ошибка при активации вашей учётной записи.</p>
<p> Пожалуйста, убедитесь, что вы переходите по корректной ссылке из письма.</p>
</div>
</div>
{% endblock %}
Они все похожи, и, по сути, можно обойтись одним шаблоном, передавая в него необходимый текст.
Заключение.
Теперь нам не страшны боты, регистрирующиеся на сайте и без какой-либо пользы раздувающие базу.
В одном из следующих постов мы добавим фоновую отправку электронной почты, используя Celery. Кроме того, я бы хотел реализовать удаление неактивированных учётных записей, скажем, раз в неделю.
Все статьи