Управление паролями в Django

Управление паролями - это то, что, как правило, не следует изобретать заново без причины, и Django стремится предоставить безопасный и гибкий набор инструментов для управления паролями пользователей. В этом документе обсуждается, как Django хранит пароли, как настроить хеширование перед сохранением, а также некоторые утилиты для управления отпечатками паролей.

Смотрите также

Даже если пользователи используют надежные пароли, злоумышленники могут подслушивать их соединения. Используйте HTTPS, чтобы избежать отправки паролей (или других конфиденциальных данных) через простые HTTP-соединения, поскольку они уязвимы для перехвата паролей.

Хранилище паролей Django

Django предоставляет гибкую систему хранения паролей и по умолчанию использует PBKDF2.

Атрибут password объекта User - это строка в следующем формате:

<algorithm>$<iterations>$<salt>$<hash>

Это компоненты, используемые для хранения пароля пользователя, разделенные символом доллара и состоящие из: алгоритма хеширования, количества итераций алгоритма (рабочий коэффициент), случайной соли. и получившийся отпечаток пароля. Алгоритм является одним из нескольких односторонних алгоритмов хранения хешей или паролей, которые может использовать Django; увидеть ниже. Итерации указывают, сколько раз алгоритм обрабатывает цифровой отпечаток пальца. Соль - это случайное начальное число, а отпечаток - результат односторонней функции.

По умолчанию Django использует алгоритм PBKDF2 с хэш-функцией SHA256, механизм растягивания пароля, рекомендованный NIST . Для большинства пользователей этого должно быть достаточно: это хорошо защищенный алгоритм, и для его работы требуется огромное количество вычислительной мощности.

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

Django выбирает, какой алгоритм использовать, глядя на настройки PASSWORD_HASHERS . Это список классов хэш-алгоритмов, которые может поддерживать установка Django. Первое вхождение этого списка (то есть settings.PASSWORD_HASHERS[0] ) будет использоваться для хранения паролей, а все остальные вхождения являются допустимыми методами хеширования, которые можно использовать для проверки существующих паролей. Это означает, что если вы хотите использовать другой алгоритм, вам придется изменить его PASSWORD_HASHERS так, чтобы ваш предпочтительный алгоритм был первым в списке.

По умолчанию PASSWORD_HASHERS :

PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
    'django.contrib.auth.hashers.Argon2PasswordHasher',
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
]

Это означает, что Django будет использовать PBKDF2 для хранения всех паролей, но будет принимать пароли, сохраненные с помощью алгоритмов PBKDF2SHA1, argon2 и bcrypt .

В следующих разделах показаны некоторые распространенные способы, которыми опытные пользователи могут изменить этот параметр.

Использование Argon2 с Django

Argon2 - победитель Конкурса хеширования паролей 2015 г. , открытого конкурса, проводимого сообществом для выбора алгоритма хеширования следующего поколения. Он разработан таким образом, чтобы его не было проще выполнять на выделенном оборудовании, чем на обычном процессоре.

Argon2 не является алгоритмом по умолчанию, используемым в Django, потому что для него требуется сторонняя библиотека. Тем не менее, эксперты Password Hashing Competition рекомендуют немедленно использовать Argon2, а не другие алгоритмы, поддерживаемые Django.

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

  1. Установите библиотеку argon2-cffi . Это можно сделать, запустив , что эквивалентно (вместе с любыми требованиями к версии от Django ).python -m pip install django[argon2] python -m pip install argon2-cffi setup.cfg

  2. Отредактируйте PASSWORD_HASHERS так, чтобы оно Argon2PasswordHasher появилось первым. Вот что должно появиться в вашем файле настроек:

    PASSWORD_HASHERS = [
        'django.contrib.auth.hashers.Argon2PasswordHasher',
        'django.contrib.auth.hashers.PBKDF2PasswordHasher',
        'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
        'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
    ]
    

    Сохраните или добавьте другие элементы в этот список, если вам нужно, чтобы Django мог обновлять пароли .

Использование bcrypt с Django

Bcrypt - это популярный алгоритм хранения паролей, специально разработанный для долгосрочного хранения паролей. Это не алгоритм Django по умолчанию, поскольку он зависит от установки сторонних библиотек. Но поскольку потенциальных пользователей много, Django поддерживает bcrypt с минимальными усилиями.

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

  1. Установите библиотеку bcrypt . Это можно сделать, запустив , что эквивалентно (вместе с любыми требованиями к версии от Django ).python -m pip install django[bcrypt] python -m pip install bcrypt setup.cfg

  2. Отредактируйте PASSWORD_HASHERS так, чтобы оно BCryptSHA256PasswordHasher появилось первым. Вот что должно появиться в вашем файле настроек:

    PASSWORD_HASHERS = [
        'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
        'django.contrib.auth.hashers.PBKDF2PasswordHasher',
        'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
        'django.contrib.auth.hashers.Argon2PasswordHasher',
    ]
    

    Сохраните или добавьте другие элементы в этот список, если вам нужно, чтобы Django мог обновлять пароли .

Итак, ваша установка Django теперь использует Bcrypt в качестве алгоритма хранения по умолчанию.

Увеличение трудоемкости

PBKDF2 и bcrypt

Алгоритмы PBKDF2 и bcrypt используют несколько итераций или хеш-проходов. Это намеренно замедляет атаки, что затрудняет взлом отпечатков пароля. Однако по мере увеличения вычислительной мощности количество требуемых итераций также увеличивается. Мы выбрали разумное значение по умолчанию (и мы будем увеличивать его с каждым новым выпуском Django), но вы можете увеличить или уменьшить его в зависимости от ваших потребностей в безопасности и возможностей вычислительной мощности. Вы можете сделать это, создав подкласс соответствующего алгоритма и переопределив параметр iterations . Например, чтобы увеличить количество итераций, используемых алгоритмом PBKDF2 по умолчанию:

  1. Создайте подкласс django.contrib.auth.hashers.PBKDF2PasswordHasher :

    from django.contrib.auth.hashers import PBKDF2PasswordHasher
    
    class MyPBKDF2PasswordHasher(PBKDF2PasswordHasher):
        """
        A subclass of PBKDF2PasswordHasher that uses 100 times more iterations.
        """
        iterations = PBKDF2PasswordHasher.iterations * 100
    

    Сохраните его где-нибудь в своем проекте. Например, вы можете поместить его в файл с именем myproject/hashers.py .

  2. Добавьте свой новый хэш-класс первым в PASSWORD_HASHERS :

    PASSWORD_HASHERS = [
        'myproject.hashers.MyPBKDF2PasswordHasher',
        'django.contrib.auth.hashers.PBKDF2PasswordHasher',
        'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
        'django.contrib.auth.hashers.Argon2PasswordHasher',
        'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
    ]
    

Вуаля, ваша установка Django теперь использует больше итераций при хранении паролей с помощью PBKDF2.

Аргон2

Argon2 имеет три атрибута, которые можно настроить:

  1. time_cost контролирует количество итераций посадочного места.
  2. memory_cost контролирует размер памяти, используемый при расчете посадочного места.
  3. parallelism контролирует количество процессоров, которые можно использовать для расчета занимаемой площади.

Значения по умолчанию для этих атрибутов, вероятно, разумны. Если вы определили, что хеширование паролей происходит слишком быстро или слишком медленно, вы можете настроить их следующим образом:

  1. Определите parallelism количество потоков, которые будут использоваться для расчета посадочного места.
  2. Определите memory_cost количество килобайт используемой памяти.
  3. Отрегулируйте time_cost и измерьте время, затрачиваемое на хеширование пароля. Выберите значение, на time_cost которое у вас уйдет приемлемое время. Если time_cost значение 1 по-прежнему слишком медленное, уменьшите значение memory_cost .

Толкование memory_cost

Утилита командной строки argon2 и некоторые другие библиотеки интерпретируют параметр memory_cost иначе, чем значение, которое использует Django. Преобразование дается .memory_cost == 2 ** memory_cost_en_ligne_de_commande

Обновление паролей

Когда пользователи входят в систему, если их пароль хранится с другим алгоритмом, чем тот, который установлен в качестве основного, Django автоматически обновляет алгоритм, чтобы использовать основной. Это означает, что старые установки Django автоматически становятся более безопасными, когда пользователи входят в систему, а также означает, что вы можете перейти на новые (и более совершенные) алгоритмы хранения по мере их появления.

Однако Django может обновлять пароли только с использованием алгоритмов, упомянутых в PASSWORD_HASHERS , поэтому важно не удалять старые алгоритмы из этого списка при переходе на новые системы. В противном случае пользователи, использующие не упомянутые алгоритмы, не смогут обновить свой пароль. Хеш-пароли обновляются, когда количество итераций PBKDF2 или раундов bcrypt увеличивается (или уменьшается).

Имейте в виду, что если все пароли в вашей базе данных не зашифрованы с помощью алгоритма хеширования по умолчанию, вы можете быть уязвимы для атаки типа перечисления учетных записей на основе времени из-за разница между продолжительностью запроса на соединение для пользователя с паролем, закодированным в другом алгоритме, и продолжительностью запроса на соединение от несуществующего пользователя (который выполняет алгоритм по умолчанию). Можно рассмотреть эту проблему в перспективе, обновив старые отпечатки пароля .

Обновление паролей без входа в систему

Если в существующей базе данных есть старые и уязвимые отпечатки паролей MD5 или SHA1, может быть желательно обновить эти отпечатки, не дожидаясь входа затронутых пользователей в систему (что может никогда не произойти, если они пользователи больше не возвращаются на сайт). В этом случае вы можете использовать "завернутый" хешер паролей.

В этом примере мы перенесем серию отпечатков SHA1, чтобы они использовали PBKDF2 (SHA1 (пароль)), и добавим соответствующий хэш пароля, который позаботится о проверке пароля пользователя во время связь. Мы предполагаем, что используется модель пользователя по умолчанию и что в проекте есть приложение accounts . Вы можете изменить этот пример для работы с любым другим алгоритмом или с пользовательской моделью пользователя.

Сначала мы добавим собственный хеш:

account / hashers.py
from django.contrib.auth.hashers import (
    PBKDF2PasswordHasher, SHA1PasswordHasher,
)


class PBKDF2WrappedSHA1PasswordHasher(PBKDF2PasswordHasher):
    algorithm = 'pbkdf2_wrapped_sha1'

    def encode_sha1_hash(self, sha1_hash, salt, iterations=None):
        return super().encode(sha1_hash, salt, iterations)

    def encode(self, password, salt, iterations=None):
        _, _, sha1_hash = SHA1PasswordHasher().encode(password, salt).split('$', 2)
        return self.encode_sha1_hash(sha1_hash, salt, iterations)

Перенос данных может выглядеть так:

account / migrations / 0002_migrate_sha1_passwords.py
from django.db import migrations

from ..hashers import PBKDF2WrappedSHA1PasswordHasher


def forwards_func(apps, schema_editor):
    User = apps.get_model('auth', 'User')
    users = User.objects.filter(password__startswith='sha1$')
    hasher = PBKDF2WrappedSHA1PasswordHasher()
    for user in users:
        algorithm, salt, sha1_hash = user.password.split('$', 2)
        user.password = hasher.encode_sha1_hash(sha1_hash, salt)
        user.save(update_fields=['password'])


class Migration(migrations.Migration):

    dependencies = [
        ('accounts', '0001_initial'),
        # replace this with the latest migration in contrib.auth
        ('auth', '####_migration_name'),
    ]

    operations = [
        migrations.RunPython(forwards_func),
    ]

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

Наконец, мы добавим настройку PASSWORD_HASHERS :

mysite / settings.py
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'accounts.hashers.PBKDF2WrappedSHA1PasswordHasher',
]

Включите в этот список любые другие хешеры, используемые вашим сайтом.

Встроенные прерыватели паролей

Полный список хешеров паролей, включенных в Django:

[
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
    'django.contrib.auth.hashers.Argon2PasswordHasher',
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
    'django.contrib.auth.hashers.BCryptPasswordHasher',
    'django.contrib.auth.hashers.SHA1PasswordHasher',
    'django.contrib.auth.hashers.MD5PasswordHasher',
    'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher',
    'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
    'django.contrib.auth.hashers.CryptPasswordHasher',
]

Соответствующие названия алгоритмов:

  • pbkdf2_sha256
  • pbkdf2_sha1
  • argon2
  • bcrypt_sha256
  • bcrypt
  • sha1
  • md5
  • unsalted_sha1
  • unsalted_md5
  • crypt

Написание собственного алгоритма хеширования

Если вы пишете свой собственный алгоритм хеширования пароля, который имеет рабочий фактор, такой как количество итераций, вы должны реализовать какой-нибудь метод, чтобы компенсировать разницу во времени выполнения между рабочим фактором, указанным в пароль и коэффициент работы алгоритма по умолчанию. Это предотвращает атаки с перечислением учетных записей на основе времени, которые используют разницу между запросами на вход пользователя, пароль которого закодирован со старым счетчиком итераций, и несуществующим пользователем (который использует номер итерации алгоритма по умолчанию).harden_runtime(self, password, encoded) encoded

В качестве примера возьмем PBKDF2, если он encoded содержит 20 000 итераций, а количество итераций по умолчанию для алгоритма равно 30 000, метод должен пройти еще password 10 000 итераций PBKDF2.

Если ваш алгоритм хеширования не имеет рабочего фактора, реализуйте метод как dummy ( pass ).

Ручное управление паролями

Модуль django.contrib.auth.hashers предоставляет набор функций для создания и проверки отпечатков пароля. Вы можете использовать их независимо от модели User .

check_password( пароль , закодированный )

Если вам нужно вручную аутентифицировать пользователя, сравнивая открытый пароль с отпечатком этого пароля в базе данных, используйте служебную функцию check_password() . Он принимает два параметра: четкий пароль для проверки и полное значение password поля пользователя в базе данных в качестве значения для сравнения; функция возвращает, True если значения совпадают, в противном случае False .

make_password( пароль , соль = None , hasher = 'default' )

Создает хешированный пароль в формате, используемом этим приложением. Требуется один обязательный аргумент: пароль в виде обычного текста (строка или байты). При желании вы можете предоставить соль и алгоритм хеширования, если вы не хотите использовать значения по умолчанию (первая запись PASSWORD_HASHERS настройки). См. В разделе « Встроенные прерыватели паролей» имя алгоритма каждого хешера. Если аргумент None пароля равен, возвращается непригодный для использования пароль (тот, который никогда не будет принят check_password() ).

Изменено в Django 3.1:

password Параметр должен быть строкой или байт , если не None .

is_password_usable( encoded_password )

Возвращает False , был ли сгенерирован пароль User.set_unusable_password() .

Проверка паролей

Пользователи часто выбирают слабые пароли. Чтобы решить эту проблему, Django предлагает дополнительную проверку пароля. Вы можете настроить несколько валидаторов паролей параллельно. Некоторые из них включены в Django, но вы также можете написать свой собственный валидатор.

Каждый валидатор паролей должен предоставлять текст справки, объясняющий требования для пользователей, проверять заданный пароль и сообщать об ошибке, если пароль не соответствует требованиям. По желанию, он может получать измененные пароли. Валидаторы также могут предлагать дополнительные настройки, чтобы специализировать свое поведение.

Проверка контролируется настройкой AUTH_PASSWORD_VALIDATORS . Список для этого параметра по умолчанию пуст, что означает, что валидатор не применяется. В новых проектах, созданных по шаблону по startproject умолчанию, активируется набор валидаторов.

По умолчанию валидаторы используются в формах для сброса или изменения паролей, а также в административных командах createsuperuser и changepassword . Валидаторы не применяются на уровне модели, например в User.objects.create_user() и create_superuser() , потому что на этом уровне разработчики, а не пользователи взаимодействуют с Django, а также потому, что проверка модели не выполняется автоматически в контекст создания моделей.

Заметка

Проверка паролей может предотвратить использование различных типов слабых паролей. Однако даже если пароль подтвержден всеми валидаторами, это не обязательно гарантирует надежный пароль. Существует множество факторов, ослабляющих надежность пароля, которые не могут быть обнаружены даже самыми продвинутыми валидаторами паролей.

Включение проверки пароля

Проверка пароля настраивается установкой AUTH_PASSWORD_VALIDATORS :

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
        'OPTIONS': {
            'min_length': 9,
        }
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

В этом примере активируются четыре встроенных валидатора:

  • UserAttributeSimilarityValidator , который проверяет схожесть между паролем и определенными атрибутами пользователя.
  • MinimumLengthValidator , который проверяет минимальную длину пароля. Этот валидатор настроен с настраиваемой опцией: для него требуется минимальная длина 9 символов вместо 8 по умолчанию.
  • CommonPasswordValidator , который проверяет, находится ли пароль в списке общих паролей. По умолчанию это сравнение производится со списком из 20 000 общих паролей.
  • NumericPasswordValidator , который проверяет, что пароль не полностью числовой.

Для UserAttributeSimilarityValidator и CommonPasswordValidator в этом примере мы используем настройки по умолчанию. NumericPasswordValidator не содержит никаких настроек.

Тексты справки по валидатору паролей и сообщения об ошибках всегда возвращаются в том порядке, в котором они появляются AUTH_PASSWORD_VALIDATORS .

Встроенные валидаторы

Django предоставляет четыре встроенных валидатора:

классMinimumLengthValidator ( min_length = 8 )

Проверяет минимальную длину паролей. Эту минимальную длину можно настроить с помощью параметра min_length .

classUserAttributeSimilarityValidator ( user_attributes = DEFAULT_USER_ATTRIBUTES , max_similarity = 0.7 )

Проверяет несоответствие пароля нескольким атрибутам пользователя.

Параметр user_attributes соответствует итерации имен атрибутов пользователя, с которыми будет выполняться сравнение. Если этот параметр опущен, список по умолчанию включает в себя: . Несуществующие атрибуты игнорируются.'username', 'first_name', 'last_name', 'email'

Минимальная степень сходства для отклоненного пароля может быть установлена ​​с помощью параметра max_similarity по шкале от 0 до 1. Значение 0 приведет к отклонению всех паролей, а значение 1 приведет только к отклонению пароли, совпадающие с одним из значений атрибута.

classCommonPasswordValidator ( password_list_path = DEFAULT_PASSWORD_LIST_PATH )

Проверяет, что пароль не соответствует текущему паролю. Пароль преобразуется в нижний регистр (для сравнения без учета регистра), а затем сравнивается со списком из 20 000 общих паролей, созданным Ройсом Уильямсом .

password_list_path может содержать путь к настраиваемому файлу общих паролей. Этот файл должен содержать один пароль в нижнем регистре в каждой строке и может быть сжатым с помощью gzip или обычным текстом.

класс NumericPasswordValidator

Убедитесь, что пароль не полностью числовой.

Интеграция валидации

Некоторые функции django.contrib.auth.password_validation можно вызывать из ваших собственных форм или кода для интеграции проверки пароля. Это может быть полезно, например, если вы используете настраиваемые формы для установки паролей или если вы пишете вызовы API для установки паролей.

validate_password( пароль , пользователь = Нет , password_validators = Нет )

Проверяет пароль. Если все валидаторы верят, что пароль действителен, None будет возвращен. Если один или несколько валидаторов отклоняют пароль, выдается исключение, ValidationError содержащее все сообщения об ошибках валидаторов.

Объект не user является обязательным: если его нет, некоторые валидаторы не смогут выполнить проверку и, следовательно, примут любой пароль.

password_changed( пароль , пользователь = Нет , password_validators = Нет )

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

Для подклассов AbstractBaseUser , поле пароля будет помечено как «грязное» при вызове, set_password() который вызывает вызов password_changed() после регистрации пользователя.

password_validators_help_texts( password_validators = Нет )

Возвращает список справочных текстов для всех валидаторов. Они объясняют требования к паролям для пользователей.

password_validators_help_text_html( password_validators = Нет )

Вернуть строку HTML со всем текстом справки в теге <ul> . Это удобно при добавлении проверки пароля в формы, поскольку позволяет передавать это содержимое непосредственно в параметр help_text поля формы.

get_password_validators( validator_config )

Возвращает набор объектов валидатора на основе параметра validator_config . По умолчанию все функции используют валидаторы, определенные в AUTH_PASSWORD_VALIDATORS , но, вызывая эту функцию с другим набором валидаторов и передавая результат в параметре password_validators других функций, затем будет использоваться этот набор валидаторов. Это полезно, когда определенный набор валидаторов применяется к большинству сценариев, но в некоторых случаях требуется другой набор. Если вы все еще используете те же валидаторы, эта функция бесполезна, потому что AUTH_PASSWORD_VALIDATORS по умолчанию используется конфигурация из .

Структура validator_config идентична структуре AUTH_PASSWORD_VALIDATORS . Значение, возвращаемое этой функцией, может быть передано в параметре password_validators функций, представленных ранее.

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

Написание собственного валидатора

Если встроенных в Django валидаторов недостаточно, вы можете написать свой собственный валидатор пароля. Валидаторы имеют довольно ограниченный интерфейс. Они должны реализовать два метода:

  • validate(self, password, user=None) : проверить пароль. Возвращает, None если пароль действителен, или генерирует ошибку ValidationError с сообщением об ошибке, если пароль недействителен. Метод должен иметь возможность управлять user допустимым пользователем None ; если это означает, что валидатор не может подать заявку, отправьте повторно, None чтобы убедиться, что ошибки нет.
  • get_help_text() : предоставляет текст справки для объяснения требований пользователю.

Все , что в OPTIONS в AUTH_PASSWORD_VALIDATORS течение вашего валидатора будет передано в конструктор. Все параметры конструктора должны иметь значение по умолчанию.

Вот базовый пример валидатора с необязательной настройкой:

from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _

class MinimumLengthValidator:
    def __init__(self, min_length=8):
        self.min_length = min_length

    def validate(self, password, user=None):
        if len(password) < self.min_length:
            raise ValidationError(
                _("This password must contain at least %(min_length)d characters."),
                code='password_too_short',
                params={'min_length': self.min_length},
            )

    def get_help_text(self):
        return _(
            "Your password must contain at least %(min_length)d characters."
            % {'min_length': self.min_length}
        )

Вы также можете реализовать ), который будет вызываться после успешной смены пароля. Это можно использовать, например, для предотвращения повторного использования пароля. Однако, если вы решили сохранить прошлые пароли пользователя, вам никогда не следует хранить пароли в открытом виде.password_changed(password, user=None

Copyright ©2021 All rights reserved