Неожиданное поведение PasswordResetView, которое отправляет слишком много последовательных писем - PullRequest
0 голосов
/ 05 мая 2020

Мой Django 3.0 Project использует вид по умолчанию Django Сброс пароля, поэтому в моем url.py:

path('password_reset', auth_views.PasswordResetView.as_view(), name='password_reset')

наблюдается странное поведение, потому что когда я запрашиваю сброс пароля, view отправляет много писем на указанный мной адрес электронной почты (скажем, 37 писем). Это произошло независимо от использования SendGrid или AWS SES, поэтому в представлении что-то не так.

Я исследовал и думаю, что проблема в классе PasswordResetForm , который представление использует для отправки электронных писем. Такая форма имеет метод get_users(), который возвращает совпадающих пользователей, которые должны получить сброс, он извлекает всех активных пользователей, отправивших электронное письмо.

def get_users(self, email):
        """Given an email, return matching user(s) who should receive a reset.
        This allows subclasses to more easily customize the default policies
        that prevent inactive users and users with unusable passwords from
        resetting their password.
        """
        email_field_name = UserModel.get_email_field_name()
        active_users = UserModel._default_manager.filter(**{
            '%s__iexact' % email_field_name: email,
            'is_active': True,
        })
        return (
            u for u in active_users
            if u.has_usable_password() and
            _unicode_ci_compare(email, getattr(u, email_field_name))
        )

Метод save() формы отправляет электронное письмо каждому из пользователей, возвращенных get_users(). Я думаю, что моя проблема в том, что я зарегистрировал несколько пользователей (скажем, 37) с одним и тем же адресом электронной почты (поскольку Django позволяет это по умолчанию), и теперь метод сохранения отправляет по одному сообщению электронной почты для каждого из этих пользователей на адрес электронной почты адрес.

def save(self, domain_override=None,
             subject_template_name='registration/password_reset_subject.txt',
             email_template_name='registration/password_reset_email.html',
             use_https=False, token_generator=default_token_generator,
             from_email=None, request=None, html_email_template_name=None,
             extra_email_context=None):
        """
        Generate a one-use only link for resetting password and send it to the
        user.
        """
        email = self.cleaned_data["email"]
        if not domain_override:
            current_site = get_current_site(request)
            site_name = current_site.name
            domain = current_site.domain
        else:
            site_name = domain = domain_override
        email_field_name = UserModel.get_email_field_name()
        for user in self.get_users(email):
            user_email = getattr(user, email_field_name)
            context = {
                'email': user_email,
                'domain': domain,
                'site_name': site_name,
                'uid': urlsafe_base64_encode(force_bytes(user.pk)),
                'user': user,
                'token': token_generator.make_token(user),
                'protocol': 'https' if use_https else 'http',
                **(extra_email_context or {}),
            }
            self.send_mail(
                subject_template_name, email_template_name, context, from_email,
                user_email, html_email_template_name=html_email_template_name,
            )

Мой вопрос: действительно ли это желательное поведение по умолчанию для Django? и как можно исправить отправку только одного электронного письма при сбросе пароля, независимо от того, сколько пользователей зарегистрировано с одним и тем же адресом электронной почты?

1 Ответ

1 голос
/ 05 мая 2020

Да, это можно довольно просто настроить. Как вы заметили, проблема в том, что PasswordResetForm отправляет электронное письмо всем пользователям. Таким образом, мы можем просто создать подкласс этой формы и настроить метод get_users. Примерно так:

class MyPasswordResetForm(PasswordResetForm):
    def get_users(self, email):
        # do what the form did before
        users = super().get_users(email)
        # choose the first user
        try:
             user = next(users)
             return [user]
        except StopIteration:  # Incase there are no users
             return []

Тогда нам нужно будет использовать новую форму в нашем PasswordResetView:

class MyPasswordResetView(PasswordResetView):
    form_class = MyPasswordResetForm

Затем вам нужно будет использовать MyPasswordResetView в urls.py где вы ранее использовали PasswordResetView.

Что касается того, почему по умолчанию возвращаются все пользователи, строка документации проливает здесь некоторый свет:

Это позволяет подклассам более легко настроить политики по умолчанию, которые не позволяют неактивным пользователям и пользователям с непригодными паролями сбрасывать свои пароли.

...