User модель, login/logout, password hashing, custom user model
Аутентификация отвечает на вопрос «кто сделал запрос». Авторизация — «может ли этот пользователь выполнить действие». Django поставляет модель пользователя, password hashing, сессии, authentication backends и готовые views; прикладной код должен использовать эти контракты, а не собирать login из отдельных полей POST.
Материал актуален для Django 5.2 LTS и 6.0.
authenticate() проверяет credentials через AUTHENTICATION_BACKENDS и возвращает пользователя либо None. login() сохраняет в сессии id пользователя, путь backend и session auth hash, а также защищается от session fixation сменой/очисткой ключа сессии в нужных случаях.
В прикладной view лучше использовать AuthenticationForm: она вызывает authenticate(), учитывает is_active стандартного backend и возвращает одинаковую ошибку для неверного логина или пароля.
from django.contrib.auth import login
from django.contrib.auth.forms import AuthenticationForm
from django.shortcuts import redirect, render
from django.views.decorators.http import require_http_methods
@require_http_methods(["GET", "POST"])
def sign_in(request):
data = request.POST if request.method == "POST" else None
form = AuthenticationForm(request=request, data=data)
if request.method == "POST" and form.is_valid():
login(request, form.get_user())
return redirect("dashboard")
return render(request, "registration/login.html", {"form": form})Этот короткий пример не включает защиту от массового перебора. Django core не ограничивает частоту попыток login: rate limit добавляют на reverse proxy или проверенным приложением с учётом NAT, распределённых workers, доверенных proxy и политики разблокировки. Для чувствительных систем также нужны MFA/passkeys и журнал событий.
Подключение включает login, logout, password change и password reset:
from django.urls import include, path
urlpatterns = [
path("accounts/", include("django.contrib.auth.urls")),
]Шаблоны по умолчанию находятся под registration/, например registration/login.html. Пути можно подключить под другим prefix, заменить отдельную view или передать ей template_name:
from django.contrib.auth.views import LoginView
path(
"sign-in/",
LoginView.as_view(template_name="accounts/sign_in.html"),
name="login",
)LOGIN_URL, LOGIN_REDIRECT_URL и LOGOUT_REDIRECT_URL принимают именованные маршруты или URL. Built-in LoginView проверяет параметр next и не перенаправляет на произвольный внешний host.
Не заменяйте это сырой строкой:
# Небезопасно: open redirect на ?next=https://attacker.example/
return redirect(request.GET.get("next", "dashboard"))Если custom workflow действительно должен обработать redirect target, применяйте url_has_allowed_host_and_scheme() с явным набором разрешённых hosts и корректным HTTPS-флагом.
Password reset состоит из четырёх views и одноразового ограниченного по времени token. Настройте почту, домен/HTTPS, нейтральный ответ, rate limit и тест полного перехода по ссылке. Не раскрывайте, существует ли email, отдельным сообщением.
В актуальном Django LogoutView выполняет logout по POST. Шаблон использует форму:
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button type="submit">Выйти</button>
</form>logout(request) полностью очищает данные текущей сессии. Если после выхода нужно сохранить неперсональное сообщение, добавляйте его после вызова logout().
from django.contrib.auth import logout
from django.shortcuts import redirect
from django.views.decorators.http import require_POST
@require_POST
def sign_out(request):
logout(request)
return redirect("home")Logout по GET уязвим для невольного запуска через ссылку, изображение или crawler. Старую ссылку <a href="...logout..."> заменяют POST-формой с CSRF.
from django.contrib.auth import get_user_model
User = get_user_model()
credentials = {User.USERNAME_FIELD: identifier}
user = User.objects.create_user(password=raw_password, **credentials)
user.set_password(new_raw_password)
user.save(update_fields=["password"])create_user() и set_password() создают hash. User.objects.create(password=raw_password) сохранит непригодную и опасную строку как будто это encoded password.
set_password() не запускает AUTH_PASSWORD_VALIDATORS; он только хэширует. Пользовательский ввод проверяют готовыми UserCreationForm, PasswordChangeForm, SetPasswordForm или явным validate_password() с пользователем.
check_password() сравнивает введённый пароль с hash. Для login используйте authenticate(), потому что backend может иметь дополнительные правила и другой USERNAME_FIELD.
Session auth hash зависит от password hash. После изменения пароля старые сессии перестают проходить проверку. Чтобы оставить текущий браузер авторизованным после контролируемой смены, Django вызывает:
from django.contrib.auth import update_session_auth_hash
user.set_password(validated_password)
user.save(update_fields=["password"])
update_session_auth_hash(request, user)Готовый PasswordChangeView/PasswordChangeForm уже решает validation и сохранение текущей сессии. Остальные сессии пользователя останутся с прежним hash и будут инвалидированы.
Если secret key ротируется через SECRET_KEY_FALLBACKS, session auth hash может временно проверяться с fallback; удаляйте старые keys по плану ротации.
Для новых password Django использует первый класс из PASSWORD_HASHERS. Остальные нужны, чтобы проверять старые hashes и постепенно обновлять их при успешном login. Django не выбирает «самый безопасный из установленных» автоматически.
По умолчанию первым стоит PBKDF2. Для Argon2id установите дополнительную зависимость и поставьте hasher первым:
python -m pip install "django[argon2]"PASSWORD_HASHERS = [
"django.contrib.auth.hashers.Argon2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
"django.contrib.auth.hashers.ScryptPasswordHasher",
]Не удаляйте legacy hasher, пока в базе остаются созданные им пароли: иначе пользователи не смогут войти и автоматически перейти на новый алгоритм. Параметры Argon2/PBKDF2 выбирают по бюджету CPU и памяти, измеряя login на production-классе инфраструктуры.
AUTH_PASSWORD_VALIDATORS проверяет качество выбранного пользователем пароля, а hasher защищает сохранённый пароль; это разные слои. Значение настройки по умолчанию пустое, хотя шаблон startproject обычно добавляет стандартный набор validators.
Встроенный django.contrib.auth.models.User использует уникальный username, но email по умолчанию не уникален. is_staff разрешает вход в admin при активном пользователе, а is_superuser заставляет permission backend считать все permissions выданными.
Поля is_staff и is_superuser независимы. create_superuser() корректно выставляет оба, но ручное присваивание is_superuser=True не меняет is_staff автоматически.
is_active=False блокирует вход через стандартные ModelBackend и формы, но смысл зависит от custom backend. Для деактивации обычно сохраняют запись ради ссылочной целостности и аудита, а не удаляют пользователя.
request.user — экземпляр user model или AnonymousUser. Проверяйте request.user.is_authenticated, не сравнивайте типы и не вызывайте это свойство как функцию.
Практичный старт — тонкий наследник AbstractUser, даже если пока добавлено одно поле:
# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
locale = models.CharField(max_length=10, default="ru")
time_zone = models.CharField(max_length=64, default="Europe/Moscow")AUTH_USER_MODEL = "accounts.User"Модель должна появиться в первой миграции приложения. Смена AUTH_USER_MODEL после появления зависимых таблиц — отдельная сложная миграция данных и связей, а не обычная правка settings.
Не задавайте custom model таблицу auth_user: это имя стандартной таблицы и создаёт конфликт или путаницу миграций.
AbstractBaseUser нужен, когда меняется сам идентификатор и контракт пользователя. Тогда разработчик обязан корректно реализовать manager, USERNAME_FIELD, обязательные поля, admin/forms и поведение flags. Для типового проекта AbstractUser дешевле и надёжнее.
Email-login требует явной политики нормализации и уникальности на уровне базы. Проверка .exists() перед созданием не защищает от гонки. Также решите, чувствителен ли local part к регистру и как обрабатываются существующие дубликаты.
В моделях используйте строку settings:
from django.conf import settings
from django.db import models
class Post(models.Model):
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name="posts",
)В runtime-коде используйте get_user_model():
from django.contrib.auth import get_user_model
User = get_user_model()Миграции, зависящие от user model, используют migrations.swappable_dependency(settings.AUTH_USER_MODEL). Не пишите raw SQL к auth_user: имя таблицы, primary key и поля custom model могут отличаться.
UserCreationForm подтверждает пароль, запускает настроенные password validators и вызывает set_password(). Для custom user создайте наследника с явным Meta.model и полями, существующими в вашей модели.
Регистрация обычно требует больше одной формы: constraint уникальности, подтверждение email, rate limit, neutral error policy, согласия и очистка незавершённых аккаунтов. Автоматический login() сразу после создания допустим только если политика продукта не требует подтверждения email или дополнительного фактора.
Ни Django User, ни AbstractUser не предоставляют готовый email verification, MFA, social login или passkeys. Используйте поддерживаемые библиотеки/провайдера и моделируйте восстановление доступа до запуска продукта.
LogoutView перестал поддерживать logout через GET в Django 5.0; актуальный путь — POST. Legacy-шаблоны со ссылкой выхода нужно обновить.
Прямой импорт User, таблица auth_user, ручной login из request.POST и собственная password form часто встречаются в старых проектах. Они не всегда являются немедленной уязвимостью, но мешают custom user, обходят validators или создают open redirect. Исправляйте их по одному, закрепляя login/logout/password-reset интеграционными тестами.
Далее: Авторизация: Permissions & Groups