В этом уроке разберём два похожих, но принципиально разных случая:
- Смена пароля — пользователь авторизован, знает текущий пароль и хочет его поменять.
- Восстановление пароля — пользователь не помнит пароль и не может войти. Нужна цепочка с email и одноразовым токеном.
Оба сценария уже реализованы в django.contrib.auth.views — нам нужно подключить шаблоны и настроить email-бэкенд.
Маршруты password_change и password_change_done уже подключены через include('django.contrib.auth.urls') из прошлого урока. Django ищет их шаблоны в папке registration/:
templates/
└── registration/
├── login.html ← уже сделан
├── password_change_form.html
└── password_change_done.html
<!-- templates/registration/password_change_form.html -->
{% extends 'base.html' %}
{% block title %}Смена пароля{% endblock %}
{% block content %}
<h1>Смена пароля</h1>
<form method="POST">
{% csrf_token %}
{% for field in form %}
<p>
{{ field.label_tag }}
{{ field }}
{% if field.errors %}
<span class="error">{{ field.errors }}</span>
{% endif %}
{% if field.help_text %}
<small>{{ field.help_text }}</small>
{% endif %}
</p>
{% endfor %}
<button type="submit">Сменить пароль</button>
</form>
{% endblock %}<!-- templates/registration/password_change_done.html -->
{% extends 'base.html' %}
{% block title %}Пароль изменён{% endblock %}
{% block content %}
<h1>Пароль успешно изменён</h1>
<p>Ваш пароль обновлён. В следующий раз используйте новый пароль для входа.</p>
<a href="{% url 'films:index' %}">На главную</a>
{% endblock %}Страница смены пароля защищена автоматически — PasswordChangeView наследует логику, которая требует авторизации. Неавторизованный пользователь будет перенаправлен на страницу входа.
Добавим ссылку на смену пароля в навигацию — например, рядом с именем пользователя в base.html:
{% if user.is_authenticated %}
| {{ user.username }}
| <a href="{% url 'password_change' %}">Сменить пароль</a>
<form method="POST" action="{% url 'logout' %}" style="display: inline;">
{% csrf_token %}
<button type="submit" class="nav-link-btn">Выйти</button>
</form>
{% endif %}Восстановление пароля — это не просто форма с полем email. Это цепочка шагов, каждый из которых решает свою задачу безопасности:
Пользователь вводит email
↓
Django проверяет, есть ли такой пользователь
↓
Создаётся одноразовый токен, привязанный к пользователю
↓
На email отправляется ссылка с токеном и uidb64
↓
Пользователь переходит по ссылке
↓
Django проверяет токен: корректен? не просрочен? соответствует пользователю?
↓
Пользователь вводит новый пароль
↓
Пароль сохраняется, токен становится недействительным
Важная деталь безопасности: Django всегда показывает одно и то же сообщение после отправки формы — независимо от того, существует email в базе или нет. Это защита от подбора аккаунтов: злоумышленник не может узнать, зарегистрирован ли конкретный email на сайте.
uidb64 — это id пользователя, закодированный в base64. token — одноразовый хэш, который Django генерирует на основе данных пользователя и временной метки. После использования или через определённое время (по умолчанию 3 дня) токен становится недействительным.
Все четыре представления восстановления пароля уже входят в django.contrib.auth.urls. Для кастомизации шаблонов достаточно создать нужные файлы в registration/. Если нужна более тонкая настройка (другой success_url, кастомный email-шаблон) — переопределяем представления явно:
# filmsite/urls.py
from django.contrib.auth.views import (
PasswordResetView,
PasswordResetDoneView,
PasswordResetConfirmView,
PasswordResetCompleteView,
)
from django.urls import reverse_lazy
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/login/', LoginView.as_view(authentication_form=CustomAuthenticationForm), name='login'),
path('accounts/register/', RegisterView.as_view(), name='register'),
# Восстановление пароля — переопределяем для указания кастомных шаблонов
path('accounts/password-reset/',
PasswordResetView.as_view(
template_name='registration/password_reset_form.html',
email_template_name='registration/password_reset_email.html',
success_url=reverse_lazy('password_reset_done'),
),
name='password_reset',
),
path('accounts/password-reset/done/',
PasswordResetDoneView.as_view(
template_name='registration/password_reset_done.html',
),
name='password_reset_done',
),
path('accounts/reset/<uidb64>/<token>/',
PasswordResetConfirmView.as_view(
template_name='registration/password_reset_confirm.html',
success_url=reverse_lazy('password_reset_complete'),
),
name='password_reset_confirm',
),
path('accounts/reset/done/',
PasswordResetCompleteView.as_view(
template_name='registration/password_reset_complete.html',
),
name='password_reset_complete',
),
path('accounts/', include('django.contrib.auth.urls')),
path('', include('films.urls')),
]Форма запроса:
<!-- templates/registration/password_reset_form.html -->
{% extends 'base.html' %}
{% block title %}Восстановление пароля{% endblock %}
{% block content %}
<h1>Восстановление пароля</h1>
<p>Укажите email, связанный с вашим аккаунтом.</p>
<form method="POST">
{% csrf_token %}
{% for field in form %}
<p>
{{ field.label_tag }}
{{ field }}
{% if field.errors %}
<span class="error">{{ field.errors }}</span>
{% endif %}
</p>
{% endfor %}
<button type="submit">Отправить письмо</button>
</form>
<p><a href="{% url 'login' %}">← Вернуться к входу</a></p>
{% endblock %}Письмо отправлено:
<!-- templates/registration/password_reset_done.html -->
{% extends 'base.html' %}
{% block title %}Письмо отправлено{% endblock %}
{% block content %}
<h1>Письмо отправлено</h1>
<p>
Если аккаунт с таким email существует, на него отправлены
инструкции по восстановлению пароля.
</p>
<p>Проверьте папку «Спам», если письмо не пришло в течение нескольких минут.</p>
{% endblock %}Шаблон самого письма — это текстовый файл, не HTML-страница:
<!-- templates/registration/password_reset_email.html -->
Вы получили это письмо, потому что был запрошен сброс пароля для аккаунта на сайте фильмов.
Перейдите по ссылке, чтобы задать новый пароль:
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
Ссылка действует 3 дня.
Если вы не запрашивали восстановление пароля — просто проигнорируйте это письмо.
Форма ввода нового пароля:
<!-- templates/registration/password_reset_confirm.html -->
{% extends 'base.html' %}
{% block title %}Новый пароль{% endblock %}
{% block content %}
<h1>Введите новый пароль</h1>
{% if validlink %}
<form method="POST">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="error">{{ form.non_field_errors }}</div>
{% endif %}
{% for field in form %}
<p>
{{ field.label_tag }}
{{ field }}
{% if field.errors %}
<span class="error">{{ field.errors }}</span>
{% endif %}
</p>
{% endfor %}
<button type="submit">Сохранить пароль</button>
</form>
{% else %}
<p>Ссылка для сброса пароля недействительна — возможно, она уже была использована или срок её действия истёк.</p>
<a href="{% url 'password_reset' %}">Запросить новую ссылку</a>
{% endif %}
{% endblock %}Переменная validlink — это булев флаг, который PasswordResetConfirmView добавляет в контекст. True — токен корректный, False — истёк или уже использован. Проверка на validlink обязательна: без неё форма будет показываться даже при переходе по устаревшей ссылке.
Завершение восстановления:
<!-- templates/registration/password_reset_complete.html -->
{% extends 'base.html' %}
{% block title %}Пароль изменён{% endblock %}
{% block content %}
<h1>Пароль успешно изменён</h1>
<p>Теперь вы можете <a href="{% url 'login' %}">войти</a> с новым паролем.</p>
{% endblock %}Добавим ссылку «Забыли пароль?» в шаблон входа:
<!-- templates/registration/login.html — добавляем после кнопки -->
<p><a href="{% url 'password_reset' %}">Забыли пароль?</a></p>Прежде чем настраивать реальную почту, убедимся, что механизм вообще работает. Django предоставляет консольный бэкенд: все письма выводятся прямо в терминал вместо отправки:
# filmsite/settings.py
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'Проверка через shell:
python manage.py shellfrom django.core.mail import send_mail
send_mail(
subject='Тест: Сайт фильмов',
message='Проверка отправки письма.',
from_email='no-reply@filmsite.local',
recipient_list=['test@example.com'],
)Если в терминале появилось письмо — бэкенд работает.
Кодировка в консоли. Тело письма может отображаться в base64 — Django кодирует UTF-8 текст для MIME. В реальном почтовом клиенте письмо декодируется автоматически. Для проверки содержимого в консоли можно использовать:
import base64 print(base64.b64decode('закодированный_текст').decode('utf-8'))
Шаг 1. Зайди в настройки Яндекс.Почты → раздел «Почтовые программы» → убедись, что доступ для почтовых клиентов по IMAP/SMTP включён.
Настройки → Почтовые программы
Шаг 2. Перейди по адресу https://id.yandex.ru/security/app-passwords и создай пароль приложения:
- Тип приложения: Почта
- Название: например,
filmsite-django
Пароль приложения показывается только один раз — сохрани его сразу. Основной пароль от почты для SMTP не подойдёт — Яндекс блокирует такие попытки.
# filmsite/settings.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.yandex.ru'
EMAIL_PORT = 465
EMAIL_USE_SSL = True
EMAIL_HOST_USER = 'mikheyxd@yandex.ru'
EMAIL_HOST_PASSWORD = 'пароль_приложения'
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER| Параметр | Значение | Назначение |
|---|---|---|
EMAIL_BACKEND |
smtp.EmailBackend |
Использовать SMTP вместо консоли |
EMAIL_HOST |
smtp.yandex.ru |
SMTP-сервер Яндекса |
EMAIL_PORT |
465 |
Порт для SSL-соединения |
EMAIL_USE_SSL |
True |
Шифрование SSL |
EMAIL_HOST_USER |
ваш email | Адрес отправителя |
EMAIL_HOST_PASSWORD |
пароль приложения | Не основной пароль от почты |
DEFAULT_FROM_EMAIL |
ваш email | Адрес в поле «От» для Django-писем |
До этого мы записывали логин и пароль прямо в settings.py:
EMAIL_HOST_USER = 'your_email@yandex.ru'
EMAIL_HOST_PASSWORD = 'пароль_приложения'Для учебных примеров это допустимо, но в реальных проектах так делать нельзя.
Причин несколько:
- пароль может случайно попасть в Git;
- доступ к репозиторию могут получить другие разработчики;
- при публикации проекта на GitHub секретные данные окажутся в открытом доступе;
- для разных окружений (локальный компьютер, тестовый сервер, production) обычно используются разные значения.
Поэтому в большинстве проектов чувствительные данные хранят в переменных окружения (Environment Variables).
Файл .env — это обычный текстовый файл, содержащий пары вида:
ИМЯ_ПЕРЕМЕННОЙ=значение
Например:
EMAIL_HOST_USER=your_email@yandex.ru
EMAIL_HOST_PASSWORD=gdtrhsudebwsgki
SECRET_KEY=django-secret-key
DEBUG=True
Такой файл не загружается в Git и хранится только на компьютере разработчика или на сервере.
Сам Python не умеет автоматически читать файл .env, поэтому обычно используют библиотеку python-dotenv.
Установим её:
pip install python-dotenvВ корне проекта создадим файл
.env
со следующим содержимым:
EMAIL_HOST_USER=your_email@yandex.ru
EMAIL_HOST_PASSWORD=gdtrhsudebwsgki
В начале settings.py подключим библиотеку:
from dotenv import load_dotenv
import osЗатем загрузим переменные окружения:
load_dotenv()Теперь можно получать значения:
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')или
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')Оба варианта используются достаточно часто.
Файл .env обязательно нужно добавить в .gitignore, чтобы случайно не отправить секретные данные в репозиторий.
# .gitignore
.env
После этого Git перестанет отслеживать этот файл.
Помимо логина и пароля SMTP, через переменные окружения обычно хранят:
SECRET_KEY;DEBUG;- параметры подключения к базе данных;
- токены Telegram-ботов;
- API-ключи сторонних сервисов;
- ключи платёжных систем;
- любые пароли и секретные данные.
Возникает логичный вопрос: если файл .env не хранится в репозитории, то как другой разработчик поймёт, какие переменные необходимо создать?
Для этого обычно создают файл .env.example.
В него записывают названия всех необходимых переменных, но без настоящих секретных данных.
Например:
EMAIL_HOST_USER=<your_email>
EMAIL_HOST_PASSWORD=<your_app_password>
SECRET_KEY=<your_secret_key>
DEBUG=True
Такой файл можно и нужно добавлять в Git.
Когда другой разработчик скачает проект, ему останется только:
- Скопировать файл
- Подставить свои значения вместо примеров.
1. Перейди на /accounts/password-reset/
2. Введи email пользователя, зарегистрированного в БД
3. Нажми «Отправить письмо»
4. Открой почтовый ящик — письмо должно прийти в течение минуты
(проверь папку «Спам», если не пришло)
5. Перейди по ссылке из письма
6. Введи новый пароль
7. Войди на сайт с новым паролем
Наиболее частая ошибка при первом подключении. Причины: используется основной пароль от почты вместо пароля приложения, опечатка в EMAIL_HOST_USER, не включён доступ для почтовых клиентов в настройках Яндекса. Решение: создать новый пароль приложения и убедиться в корректности всех параметров.
Скорее всего письмо попало в «Спам» — особенно часто при отправке с домена localhost или 127.0.0.1. Для тестов используй реальный почтовый ящик с доступом.
Без проверки {% if validlink %} в password_reset_confirm.html пользователь будет видеть форму ввода пароля даже при переходе по устаревшей ссылке — после отправки получит непонятную ошибку. Всегда проверяй validlink и показывай понятное сообщение для недействительных токенов.
Яндекс поддерживает два варианта:
# SSL (порт 465) — рекомендуется
EMAIL_PORT = 465
EMAIL_USE_SSL = True
# TLS/STARTTLS (порт 587) — альтернатива
EMAIL_PORT = 587
EMAIL_USE_TLS = TrueНельзя включать оба одновременно — EMAIL_USE_SSL и EMAIL_USE_TLS взаимоисключающие.
- Чем смена пароля отличается от восстановления с точки зрения требований к пользователю?
- Почему Django всегда показывает одно и то же сообщение после отправки формы восстановления — независимо от того, существует email или нет?
- Что такое
validlinkв контекстеPasswordResetConfirmViewи зачем его проверять в шаблоне? - Почему нельзя использовать основной пароль от Яндекс-почты в
EMAIL_HOST_PASSWORD? - Чем
EMAIL_USE_SSLотличается отEMAIL_USE_TLSи можно ли включить оба?
Тип: расширь проект
Часть 1. Создай все четыре шаблона для восстановления пароля и добавь ссылку «Забыли пароль?» на страницу входа.
Часть 2. Настрой консольный бэкенд и проверь работу через браузер: запроси восстановление пароля для существующего пользователя, найди ссылку в консоли (или расшифруй base64), перейди по ней и смени пароль.
Часть 3. Переключись на SMTP-бэкенд с Яндекс-почтой. Создай пароль приложения, заполни настройки в settings.py и проверь, что письмо доходит до реального почтового ящика.
Часть 4 (опционально). Вынеси EMAIL_HOST_USER и EMAIL_HOST_PASSWORD в переменные окружения — не храни учётные данные прямо в settings.py.
Ошибка ssl.SSLCertVerificationError
При настройке отправки почты через SMTP (например, для восстановления пароля в Django) можно столкнуться с ошибкой:
ssl.SSLCertVerificationError:
[SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed:
unable to get local issuer certificate
Эта ошибка означает, что Python не может проверить SSL-сертификат SMTP-сервера, так как в системе отсутствует (или не подключено) хранилище доверенных корневых сертификатов.
Важно понимать, что проблема не связана с Django, SMTP или настройками почты — она возникает на уровне Python.
Попробуйте подключиться к SMTP напрямую:
import smtplib
import ssl
context = ssl.create_default_context()
with smtplib.SMTP_SSL("smtp.yandex.ru", 465, context=context) as server:
server.login("your_email@yandex.ru", "пароль_приложения")Если появляется та же ошибка CERTIFICATE_VERIFY_FAILED, значит проблема точно не в Django.
Выполните:
import ssl
print(ssl.get_default_verify_paths())Если вывод похож на следующий:
DefaultVerifyPaths(
cafile=None,
capath=None,
...
)то Python не смог найти доверенные сертификаты.
Также можно проверить количество загруженных сертификатов:
import ssl
print(len(ssl.create_default_context().get_ca_certs()))В норме должно быть примерно 100–200 сертификатов.
Если Python установлен с официального сайта, после установки необходимо выполнить специальный скрипт:
Install Certificates.command
Он находится здесь:
/Applications/Python 3.x/
Например:
/Applications/Python 3.13/
Запустить его можно двумя способами.
Откройте папку:
Applications → Python 3.13
и дважды щелкните по файлу
Install Certificates.command
open "/Applications/Python 3.13/Install Certificates.command"После выполнения скрипта Python автоматически подключит системное хранилище сертификатов.
Снова выполните:
import ssl
print(len(ssl.create_default_context().get_ca_certs()))Теперь количество сертификатов должно быть значительно больше нуля.
После этого SMTP обычно начинает работать без каких-либо дополнительных настроек.
После исправления SSL может появиться ошибка:
UnicodeEncodeError:
'ascii' codec can't encode characters...
Она означает, что SMTP не смог закодировать логин или пароль.
Чаще всего причина одна из двух:
- в
EMAIL_HOST_USERуказан не настоящий адрес электронной почты, а текст вроде
EMAIL_HOST_USER = 'ваш_адрес@yandex.ru'- либо в логине присутствуют символы, отличные от ASCII.
Правильно:
EMAIL_HOST_USER = 'my_email@yandex.ru'
EMAIL_HOST_PASSWORD = 'пароль_приложения'Практически всегда — нет.
На большинстве современных Linux-дистрибутивов (Ubuntu, Debian, Rocky Linux и др.) пакет ca-certificates уже установлен, поэтому Python сразу использует системное хранилище сертификатов.
Поэтому на VPS подобная ошибка встречается крайне редко.
