Пользователь должен иметь возможность вводить данные, принадлежащие только ему лично. Мы создадим систему, которая будет определять, какому пользователю принадлежат те или иные данные, и ограничивать доступ к страницам, чтобы пользователь мог работать только с принадлежащими ему данными.
В этом разделе мы изменим модель Topic, чтобы каждая тема принадлежала конкретному пользователю. При этом также автоматически решается проблема с записями, так как каждая запись принадлежит конкретной теме. Начнем с ограничения доступа к страницам.
Ограничение доступа с использованием @login_required
Django позволяет легко ограничить доступ к определенным страницам для пользователей, выполнивших вход, с помощью декоратора @login_required. Декоратор (decorator) представляет собой директиву, размещенную непосредственно перед определением функции, применяемую к функции перед ее выполнением и влияющую на поведение кода. Рассмотрим пример.
Ограничение доступа к страницам тем
Каждая тема будет принадлежать пользователю, поэтому только зарегистрированные пользователи смогут запрашивать страницы тем. Добавьте следующий код в learning_logs/views.py:
views.py
...
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from .models import Topic, Entry
...
@login_required
def topics(request):
"""Выводит все темы."""
...
Сначала импортируется функция login_required(). Мы применяем login_required() как декоратор для функции представления topics(), для чего перед именем login_required() ставится знак @; он сообщает Python, что этот код должен выполняться перед кодом topics().
Код login_required() проверяет, выполнил ли пользователь вход, и Django выполняет код topics() только при выполнении этого условия. Если же пользователь не выполнил вход, он перенаправляется на страницу входа.
Чтобы перенаправление работало, необходимо внести изменения settings.py и сообщить Django, где искать страницу входа. Добавьте следующий фрагмент в самый конец settings.py:
settings.py
"""
Django settings for learning_log project
...
# Мои настройки
LOGIN_URL = '/users/login/'
Когда пользователь, не прошедший проверку, запрашивает страницу, защищенную декоратором @login_required, Django отправляет пользователя на URL-адрес, определяемый LOGIN_URL в settings.py.
Чтобы протестировать эту возможность, завершите сеанс в любой из своих учетных записей и вернитесь на домашнюю страницу. Щелкните на ссылке Topics, которая должна направить вас на страницу входа. Выполните вход с любой из своих учетных записей, на домашней странице снова щелкните на ссылке Topics. На этот раз вы получите доступ к странице со списком тем.
Ограничение доступа в Learning Log
Django упрощает ограничение доступа к страницам, но вы должны решить, какие страницы следует защищать. Лучше сначала подумать, к каким страницам можно разрешить неограниченный доступ, а затем ограничить его для всех остальных страниц. Снять излишние ограничения несложно, причем это куда менее рискованно, чем оставлять действительно важные страницы без ограничения доступа.
В приложении Learning Log мы оставим неограниченный доступ к домашней странице, странице регистрации и выхода. Доступ ко всем остальным страницам будет ограничен.
Вот как выглядит файл learning_logs/views.py с декораторами @login_required, примененными к каждому представлению, кроме index():
views.py
...
@login_required
def topics(request):
. ....
@login_required
def topic(request, topic_id):
. ....
@login_required
def new_topic(request):
. ....
. .
@login_required
def new_entry(request, topic_id):
. ....
@login_required
def edit_entry(request, entry_id):
. ....
Попробуйте обратиться к любой из этих страниц без выполнения входа: вы будете перенаправлены обратно на страницу входа. Кроме того, вы не сможете щелкать на ссылках на такие страницы, как new_topic. Но если ввести URL http://localhost:8000/new_topic/, вы будете перенаправлены на страницу входа. Ограничьте доступ ко всем URL-адресам, связанным с личными данными пользователей.
Связывание данных с конкретными пользователями
Теперь данные, отправленные пользователем, необходимо связать с тем пользователем, который их отправил. Связь достаточно установить только с данными, находящимися на высшем уровне иерархии, а низкоуровневые данные последуют за ними автоматически. Например, в приложении Learning Log на высшем уровне находятся темы, а каждая запись связывается с некоторой темой. Если каждая тема принадлежит конкретному пользователю, мы сможем отследить владельца каждой записи в базе данных.
Изменим модель Topic и добавим отношение внешнего ключа с пользователем. После этого необходимо провести миграцию базы данных. Наконец, необходимо изменить некоторые представления, чтобы в них отображались только данные, связанные с текущим пользователем.
Изменение модели Topic
В файле models.py изменяются всего две строки:
models.py
from django.db import models
from django.contrib.auth.models import User
class Topic(models.Model):
"""Тема, которую изучает пользователь"""
text = models.CharField(max_length=200)
date_added = models.DateTimeField(auto_now_add=True)
. .owner = models.ForeignKey(User)
def __str__(self):
"""Возвращает строковое представление модели."""
return self.text
class Entry(models.Model):
...
Сначала модель User импортируется из django.contrib.auth. Затем в Topic добавляется поле owner, используемое в отношении внешнего ключа с моделью User.
Идентификация существующих пользователей
При проведении миграции Django модифицирует базу данных, чтобы в ней хранилась связь между каждой темой и пользователем. Для выполнения миграции Django необходимо знать, с каким пользователем должна быть связана каждая существующая тема. Проще всего связать все существующие темы с одним пользователем, например суперпользователем. Но для этого сначала необходимо узнать идентификатор этого пользователя.
Просмотрим идентификаторы всех пользователей, созданных до настоящего момента. Запустите сеанс оболочки Django и введите следующие команды:
(venv)learning_log$ python manage.py shell
(1) >>>from django.contrib.auth.models import User
(2)>>>User.objects.all()
[
(3)>>>for user in User.objects.all():
... . . print(user.username, user.id)
...
ll_admin 1
eric 2
willie 3
>>>
В точке (1) в сеанс оболочки импортируется модель User. После этого просматриваются все пользователи, созданные до настоящего момента (2). В выходных данных перечислены три пользователя: ll_admin, eric и willie.
В точке (3) перебирается список пользователей, и для каждого пользователя выводится его имя и идентификатор. Когда Django спросит, с каким пользователем связать существующие темы, мы используем один из этих идентификаторов.
Миграция базы данных
Зная значение идентификатора, можно провести миграцию базы данных.
(1) (venv)learning_log$ python manage.py makemigrations learning_logs
(2)You are trying to add a non-nullable field 'owner' to topic without a default;
we can't do that (the database needs something to populate existing rows).
(3)Please select a fix:
1) Provide a one-off default now (will be set on all existing rows)
2) Quit, and let me add a default in models.py
(4)Select an option: 1
(5)Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do
e.g. timezone.now()
? >>> 1
Migrations for 'learning_logs':
0003_topic_owner.py:
. .- Add field owner to topic
Сначала выдается команда makemigrations (1) . В ее выходных данных (2) Django сообщает, что мы пытаемся добавить обязательное поле (значения которого отличны от null) в существующую модель (topic) без указания значения по умолчанию. Django предоставляет два варианта (3): мы можем либо указать значение по умолчанию прямо сейчас, либо завершить выполнение программы и добавить значение по умолчанию в models.py. В точке (4) выбирается первый вариант. Тогда Django запрашивает значение по умолчанию (5).
Чтобы связать все существующие темы с исходным административным пользователем ll_admin, я ввел в точке ? идентификатор пользователя 1. Вы можете использовать идентификатор любого из созданных пользователей; он не обязан быть суперпользователем. Django проводит миграцию базы данных, используя это значение, и создает файл миграции 0003_topic_owner.py, добавляющий поле owner в модель Topic.
Теперь можно провести миграцию. Введите следующую команду в активной виртуальной среде:
(venv)learning_log$ python manage.py migrate
Operations to perform:
Synchronize unmigrated apps: messages, staticfiles
Apply all migrations: learning_logs, contenttypes, sessions, admin, auth
...
Running migrations:
Rendering model states... DONE
(1) Applying learning_logs.0003_topic_owner... OK
(venv)learning_log$
Django применяет новую миграцию с результатом OK (1) . Чтобы убедиться в том, что миграция сработала так, как и ожидалось, можно воспользоваться интерактивной оболочкой:
(1) >>>from learning_logs.models import Topic
(2)>>>for topic in Topic.objects.all():
... . . print(topic, topic.owner)
...
Chess ll_admin
Rock Climbing ll_admin
>>>
После импортирования Topic из learning_logs.models (1) мы перебираем все существующие темы, выводим каждую тему и имя пользователя, которому она принадлежит (2). Как видите, сейчас каждая тема принадлежит пользователю ll_admin.
Примечание
Вместо миграции можно просто сбросить содержимое базы данных, но это приведет к потере всех существующих данных. Полезно научиться выполнять миграцию базы данных без нарушения целостности данных пользователей. Если вы хотите начать с новой базы данных, используйте команду python manage.py flush для повторного построения структуры базы данных. Вам придется создать нового суперпользователя, а все данные будут потеряны.
Ограничение доступа к темам
В настоящее время пользователь, выполнивший вход, будет видеть все темы независимо от того, под какой учетной записью он вошел. Сейчас мы изменим приложение, чтобы каждый пользователь видел только принадлежащие ему темы.
Внесите следующее изменение в функцию topics() в файле views.py:
views.py
...
@login_required
def topics(request):
"""Выводит список тем."""
. .topics = Topic.objects.filter(owner=request.user).order_by('date_added')
context = {'topics': topics}
return render(request, 'learning_logs/topics.html', context)
...
Если пользователь выполнил вход, в объекте запроса устанавливается атрибут request.user с информацией о пользователе. Фрагмент кода Topic.objects.filter(owner=request.user) приказывает Django извлечь из базы данных только те объекты Topic, у которых атрибут owner соответствует текущему пользователю. Так как способ отображения не изменяется, изменять шаблон для страницы тем вообще не нужно.
Чтобы увидеть, как работает этот способ, выполните вход в качестве пользователя, с которым связаны все существующие темы, и перейдите к странице со списком тем. На ней должны отображаться все темы. Теперь завершите сеанс и войдите снова с другой учетной записью. На этот раз страница должна быть пустой.
Защита тем пользователя
Никаких реальных ограничений на доступ к страницам еще не существует, поэтому любой зарегистрированный пользователь может опробовать разные URL (например, http://localhost:8000/topics/1/) и просмотреть страницы тем, которые ему удастся подобрать.
Попробуйте сделать это. После входа с учетной записью суперпользователя скопируйте URL или запишите идентификатор в URL темы, после чего завершите сеанс и войдите снова от имени другого пользователя. Введите URL этой темы. Вам удастся прочитать все записи, хотя сейчас вы вошли под именем другого пользователя.
Чтобы решить эту проблему, мы будем выполнять проверку перед получением запрошенных данных в функции представления topic():
views.py
from django.shortcuts import render
(1) from django.http import HttpResponseRedirect, Http404
from django.core.urlresolvers import reverse
...
@login_required
def topic(request, topic_id):
"""Выводит одну тему и все ее записи."""
topic = Topic.objects.get(id=topic_id)
. .# Проверка того, что тема принадлежит текущему пользователю.
(2) . .if topic.owner != request.user:
. . . .raise Http404
. . . .
entries = topic.entry_set.order_by('-date_added')
context = {'topic': topic, 'entries': entries}
return render(request, 'learning_logs/topic.html', context)
...
Код 404 — стандартное сообщение об ошибке, которое возвращается в тех случаях, когда запрошенный ресурс не существует на сервере. В данном случае мы импортируем исключение Http404 (1) , которое будет выдаваться программой при запросе пользователем темы, которую ему видеть не положено. Получив запрос темы, перед отображением страницы мы убеждаемся в том, что пользователь этой темы является текущим пользователем приложения. Если тема не принадлежит текущему пользователю, выдается исключение Http404 (2), а Django возвращает страницу с ошибкой 404.
Пока при попытке просмотреть записи другого пользователя вы получите от Django сообщение «Страница не найдена». В главе 20 проект будет настроен так, чтобы пользователь видел полноценную страницу ошибки.
Защита страницы edit_entry
Страницы edit_entry используют URL-адреса в форме http://localhost:8000/edit_entry/entry_id/, где entry_id — число. Защитим эту страницу, чтобы никто не мог подобрать URL для получения доступа к чужим записям:
views.py
...
@login_required
def edit_entry(request, entry_id):
"""Редактирует существующую запись."""
entry = Entry.objects.get(id=entry_id)
topic = entry.topic
. .if topic.owner != request.user:
. . . .raise Http404
if request.method != 'POST':
# Исходный запрос; форма заполняется данными текущей записи.
...
Программа читает запись и тему, связанную с этой записью. Затем мы проверяем, совпадает ли владелец темы с текущим пользователем; при несовпадении выдается исключение Http404.
Связывание новых тем с текущим пользователем
В настоящее время страница добавления новых тем несовершенна, потому что она не связывает новые темы с конкретным пользователем. При попытке добавить новую тему выдается сообщение об ошибке IntegrityError с уточнением learning_logs_topic.user_id may not be NULL. Django говорит, что при создании новой темы обязательно должно быть задано значение поля owner.
Проблема легко решается, потому что мы можем получить доступ к информации текущего пользователя через объект request. Добавьте следующий код, связывающий новую тему с текущим пользователем:
views.py
...
@login_required
def new_topic(request):
"""Определяет новую тему."""
if request.method != 'POST':
# Данные не отправлялись; создается пустая форма.
form = TopicForm()
else:
# Отправлены данные POST; обработать данные.
form = TopicForm(request.POST)
if form.is_valid():
(1) . . . . . .new_topic = form.save(commit=False)
(2) . . . . . .new_topic.owner = request.user
(3) . . . . . .new_topic.save()
return HttpResponseRedirect(reverse('learning_logs:topics'))
. . . . . .
context = {'form': form}
return render(request, 'learning_logs/new_topic.html', context)
...
При первом вызове form.save() передается аргумент commit=False, потому что новая тема должна быть изменена перед сохранением в базе данных (1) . Атрибуту owner новой темы присваивается текущий пользователь (2). Наконец, мы вызываем save() для только что определенного экземпляра темы (3). Теперь тема содержит все обязательные данные, и ее сохранение пройдет успешно.
Вы сможете добавить сколько угодно новых тем для любого количества разных пользователей. Каждому пользователю будут доступны только его собственные данные, какие бы операции он ни пытался выполнять: просмотр данных, ввод новых или изменение существующих данных.
Упражнения
19-3. Рефакторинг: в views.py есть два места, в которых программа проверяет, что пользователь, связанный с темой, является текущим пользователем. Поместите код этой проверки в функцию с именем check_topic_owner() и вызовите эту функцию при необходимости.
19-4. Защита new_entry: пользователь может попытаться добавить новую запись в журнал другого пользователя, вводя URL-адрес с идентификатором темы, принадлежащей другому пользователю. Чтобы предотвратить подобные атаки, перед сохранением новой записи проверьте, что текущий пользователь является владельцем темы, к которой относится запись.
19-5. Защищенный блог: в проекте Blog примите меры к тому, чтобы каждое сообщение в блоге было связано с конкретным пользователем. Убедитесь в том, что чтение всех сообщений доступно всем пользователям, но только зарегистрированные пользователи могут создавать новые и редактировать существующие сообщения. В представлении, в котором пользователи редактируют сообщения, перед обработкой формы убедитесь в том, что редактируемое сообщение принадлежит именно этому пользователю.