Пять модулей мы строили сайт, где любой пользователь может делать всё: добавлять фильмы, редактировать, удалять, оставлять рецензии. В реальном приложении это неприемлемо — разные действия доступны разным пользователям.
Django поставляется с полноценной системой авторизации: пользователи, пароли, сессии, разрешения, группы. Всё это входит в django.contrib.auth, который уже давно подключён в INSTALLED_APPS с самого первого урока и создал свои таблицы при первом migrate. Мы просто ещё не использовали это.
Сегодня разберём первую половину модуля: вход и выход из системы, защита представлений через декоратор и Mixin.
django.contrib.auth.models.User — это таблица пользователей, которую Django создал при первом migrate. Суперпользователь, которого мы создали в уроке 13 через createsuperuser — это уже объект этой модели.
Основные поля:
| Поле | Назначение |
|---|---|
id |
Первичный ключ |
username |
Уникальное имя пользователя |
email |
Электронная почта |
password |
Хэш пароля (не сам пароль) |
first_name, last_name |
Имя и фамилия |
is_active |
Активен ли аккаунт |
is_staff |
Есть ли доступ к /admin/ |
is_superuser |
Суперпользователь — все права без явного назначения |
date_joined |
Дата регистрации |
last_login |
Дата последнего входа |
Важно: Django никогда не хранит пароль в открытом виде — только его хэш через PBKDF2 (по умолчанию). При проверке пароля Django хэширует введённое значение и сравнивает хэши.
В каждом представлении текущий пользователь доступен через request.user. Это либо объект User, либо AnonymousUser, если пользователь не вошёл в систему:
def some_view(request):
if request.user.is_authenticated:
print(request.user.username) # имя авторизованного пользователя
else:
print('Анонимный пользователь')is_authenticated — это атрибут, не метод. Для объекта User он всегда True, для AnonymousUser — всегда False.
Django предоставляет готовые представления для входа и выхода. Их можно подключить одной строкой:
# filmsite/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('django.contrib.auth.urls')), # все маршруты авторизации
path('', include('films.urls')),
]django.contrib.auth.urls добавляет следующие маршруты:
| Маршрут | Имя | Назначение |
|---|---|---|
accounts/login/ |
login |
Форма входа |
accounts/logout/ |
logout |
Выход |
accounts/password_change/ |
password_change |
Смена пароля |
accounts/password_change/done/ |
password_change_done |
Подтверждение смены |
accounts/password_reset/ |
password_reset |
Восстановление пароля |
accounts/password_reset/done/ |
password_reset_done |
Письмо отправлено |
accounts/reset/<uidb64>/<token>/ |
password_reset_confirm |
Форма нового пароля |
accounts/reset/done/ |
password_reset_complete |
Пароль изменён |
Сегодня разберём login и logout, остальные — в уроках 24 и 25.
# filmsite/settings.py
# Куда перенаправлять после успешного входа
LOGIN_REDIRECT_URL = '/' # на главную страницу
# Куда перенаправлять после выхода
LOGOUT_REDIRECT_URL = '/'
# Куда перенаправлять неавторизованных пользователей
# (используется login_required и LoginRequiredMixin)
LOGIN_URL = '/accounts/login/'LoginView из django.contrib.auth.views — это готовое CBV-представление, которое показывает форму входа (GET) и проверяет учётные данные (POST). При успешном входе перенаправляет на LOGIN_REDIRECT_URL или на страницу, которую пользователь пытался открыть до перенаправления (параметр next).
LoginView уже подключён через django.contrib.auth.urls. Осталось только создать шаблон — Django будет искать его в папке registration/:
templates/
└── registration/
└── login.html
<!-- templates/registration/login.html -->
{% extends 'base.html' %}
{% block title %}Вход — Сайт фильмов{% endblock %}
{% block content %}
<h1>Вход на сайт</h1>
{% if form.errors %}
<div class="error">
<p>Неверное имя пользователя или пароль. Попробуйте снова.</p>
</div>
{% endif %}
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<input type="hidden" name="next" value="{{ next }}">
<button type="submit">Войти</button>
</form>
{% comment %} <p>Ещё нет аккаунта? <a href="{% url 'register' %}">Зарегистрироваться</a></p> {% endcomment %}
{% endblock %}Скрытое поле next сохраняет адрес, куда пользователь должен вернуться после входа. Если пользователь пытался открыть /films/add/, но был перенаправлен на страницу входа, next сохраняет /films/add/, и LoginView вернёт пользователя туда после успешной авторизации.
Выход реализован так же просто. Поскольку выход — это действие, изменяющее состояние (завершает сессию), оно должно выполняться через POST, а не через GET-ссылку:
<!-- В шапке сайта — например, в base.html -->
{% if user.is_authenticated %}
<span>{{ user.username }}</span>
<form method="POST" action="{% url 'logout' %}" style="display: inline;">
{% csrf_token %}
<button type="submit">Выйти</button>
</form>
{% else %}
<a href="{% url 'login' %}">Войти</a>
{% endif %}Django 5.x требует POST для выхода — GET-запрос на
/accounts/logout/больше не завершает сессию. Это изменение было введено в Django 5.0 по соображениям безопасности (защита от CSRF).
Переменная user доступна в шаблонах без явной передачи в контекст — её добавляет контекстный процессор django.contrib.auth.context_processors.auth, который уже подключён в TEMPLATES → 'OPTIONS' → 'context_processors' в settings.py.
Декоратор login_required проверяет, авторизован ли пользователь, перед вызовом функции. Если нет — перенаправляет на LOGIN_URL:
# films/views.py
from django.contrib.auth.decorators import login_required
@login_required
def add_film(request):
# Этот код выполнится только для авторизованных пользователей
...
@login_required
def add_review(request, slug):
...Применять декоратор можно и в urls.py, не меняя сам файл представлений:
# films/urls.py
from django.contrib.auth.decorators import login_required
from . import views
urlpatterns = [
path('films/add/', login_required(views.add_film), name='add_film'),
]# Указать свой URL входа
@login_required(login_url='/my-login/')
# Указать redirect_field_name (по умолчанию 'next')
@login_required(redirect_field_name='redirect_to')Для классовых представлений используется LoginRequiredMixin, который мы уже подключали в прошлом уроке для FilmCreateView и других представлений. Теперь он будет работать по-настоящему — потому что мы настроили LOGIN_URL:
# films/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
class FilmCreateView(LoginRequiredMixin, CreateView):
model = Film
form_class = FilmForm
template_name = 'films/add_film.html'
class FilmUpdateView(LoginRequiredMixin, FilmEditMixin, UpdateView):
context_object_name = 'film'
def get_success_url(self):
return self.object.get_absolute_url()
class FilmDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
model = Film
permission_required = 'films.delete_film'
template_name = 'films/film_confirm_delete.html'
success_url = reverse_lazy('films:film_list')Как работает LoginRequiredMixin: он переопределяет метод dispatch() — точку входа, которую View.as_view() вызывает для каждого запроса. До того как запрос попадёт в get() или post(), dispatch() проверяет request.user.is_authenticated. Если нет — делает редирект. Именно поэтому Mixin должен стоять первым в списке наследования.
Обновим base.html, чтобы навигация учитывала статус авторизации:
<!-- templates/base.html -->
{% load static %}
{% load film_tags %}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>{% block title %}Сайт фильмов{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/base.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body>
<header>
<nav>
<a href="{% url 'films:index' %}">Главная</a> |
<a href="{% url 'films:film_list' %}">Каталог</a> |
<a href="{% url 'films:about' %}">О сайте</a>
<span class="nav-auth">
{% if user.is_authenticated %}
| <a href="{% url 'films:add_film' %}">Добавить фильм</a>
| {{ user.username }}
<form method="POST" action="{% url 'logout' %}" style="display: inline;">
{% csrf_token %}
<button type="submit" class="nav-link-btn">Выйти</button>
</form>
{% else %}
| <a href="{% url 'login' %}">Войти</a>
{% endif %}
</span>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
{% latest_films 3 %}
<footer>
<p>© {% current_year %} Сайт фильмов</p>
</footer>
{% block extra_js %}{% endblock %}
</body>
</html>В Django до версии 5.0 выход можно было выполнить простой ссылкой <a href="{% url 'logout' %}">. Начиная с Django 5.0 это не работает — LogoutView принимает только POST. Если на проекте стоит Django 5.x, а в шаблоне осталась GET-ссылка для выхода — пользователи не смогут выйти без видимой ошибки (их просто перенаправят на главную без завершения сессии).
Нельзя применить @login_required непосредственно к классу CBV — только к результату as_view():
# Неправильно — декоратор применяется к классу, а не к функции
@login_required
class FilmCreateView(CreateView):
...
# Через Mixin — рекомендуемый способ для CBV
class FilmCreateView(LoginRequiredMixin, CreateView):
...
# Через as_view() в urls.py — работает, но Mixin удобнее
path('films/add/', login_required(FilmCreateView.as_view()), name='add_film'),Если неавторизованный пользователь пытается открыть /films/add/, Django перенаправит его на /accounts/login/?next=/films/add/. После успешного входа LoginView прочитает параметр next и вернёт пользователя на /films/add/. Но если в форме входа не передать <input type="hidden" name="next" value="{{ next }}"> — значение next потеряется при отправке POST-запроса. Шаблон registration/login.html обязательно должен содержать это скрытое поле.
- Что возвращает
request.userдля неавторизованного пользователя, и как проверить статус авторизации? - Чем отличается
login_requiredотLoginRequiredMixinи когда применять каждый? - Почему выход из системы в Django 5.x должен выполняться через POST-форму, а не через ссылку?
- Для чего служит скрытое поле
<input type="hidden" name="next">в шаблоне формы входа? - Откуда переменная
userберётся в шаблонах без явной передачи в контекст?
Тип: расширь проект
Защити операции записи в каталоге и обнови навигацию.
Требования:
- Добавь
LoginRequiredMixinко всем CBV, которые изменяют данные:FilmCreateView,FilmUpdateView,FilmDeleteView,AddReviewView,DirectorCreateView,DirectorUpdateView,DirectorDeleteView - Для FBV
search_filmиcatalog_statsзащита не нужна — они только читают данные - Обнови
base.html: кнопки «Добавить фильм» и «Выйти» показываются только авторизованным пользователям; неавторизованным — ссылка «Войти» - Создай шаблон
templates/registration/login.htmlсо скрытым полемnext - Проверь: открой
/films/add/без входа — должно перенаправить на страницу входа; после входа — вернуть обратно на/films/add/