Cat

Django 39. Капча и подтверждение регистрации по email

В этом посте подключим к сайту Google reCAPTCHA для защиты от ботов и реализуем подтверждение регистрации по электронной почте.

Все статьи

Icon Link

Дополнительные материалы

Icon Link

Реклама

Icon Link
Сайт на Django proDream 09 Январь 2024 Просмотров: 1182

Когда мы с вами работали над формой регистрации, мы упустили одну важную деталь – ботов. Если ничего не предпринимать, боты будут регистрироваться на сайте в большом количестве, нагружая базу данных заведомо неактуальной информацией, плодя спам в комментариях под постами и доставляя немало других мелких и не очень неприятностей.

Для решения этой проблемы мы сделаем следующее:

  1. Добавим на страницу регистрации Google reCAPTCHA, для явной защиты от ботов и массовых регистраций. Да, это не самая лучшая защита от ботов, но 99% всё же будут отсеяны.
  2. Добавим обязательное подтверждение регистрации по 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) в конце метода, то активация работать не будет. Оказывается, генерация токена берёт за основу время изменения пользователя и получается следующая последовательность:

  1. Регистрация пользователя - первое время изменения
  2. Деактивация пользователя и сохранение - второе время изменения
  3. Отправка письма с токеном на основе времени изменения из пункта 2.
  4. Вызов 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")

 

Представление страницы активации и завершения регистрации.

Сделаем два представления:

  1. На первое будем перенаправлять после регистрации. Оно будет просто выводить страницу с текстом, не более.
  2. На второе пользователь будет попадать перейдя по ссылке из письма. В нём будет логика валидации токена и активации пользователя.

Начнём с первого представления. Создадим класс CustomRegistrationDoneView, унаследованный от TemplateView.
В нём пропишем два поля:

  1. template_name - файл шаблона страницы.
  2. 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. Кроме того, я бы хотел реализовать удаление неактивированных учётных записей, скажем, раз в неделю.

Автор

    Нет комментариев

    Реклама