Производительность и безопасность

Рабочее приложение и приложение, готовое к проду, — разные вещи: первое отвечает на запросы, второе делает это быстро и не отдаёт лишнего злоумышленнику. Этот урок — про инструменты диагностики и оборонительный чеклист перед выкаткой.

Готовность к продакшену — это сочетание производительности (приложение держит нагрузку, не делая лишней работы) и безопасности (оно защищено от типовых атак и не раскрывает секреты). Django даёт инструменты для обоих: профайлер запросов и встроенную проверку настроек.

Когда базовый деплой позади, наступает другой класс задач. Под нагрузкой вылезает то, чего не видно на одном разработчике: страница, делающая сотню запросов вместо трёх; DEBUG=True, забытый включённым и показывающий трассировку с кодом всем подряд; секретный ключ, лежащий прямо в репозитории. Эти проблемы редко «крэшат» приложение — оно работает, просто медленно и небезопасно. Поэтому их ловят не тестами на падение, а специальными инструментами диагностики и дисциплиной чеклиста.

Зачем это на практике

Реальная история повторяется в каждом втором проекте: страница списка заказов открывается за 4 секунды, разработчик грешит на «медленную базу» и просит мощнее сервер. А django-debug-toolbar показывает 312 одинаковых запросов SELECT * FROM customer WHERE id = ? — классический N+1, который чинится одной строкой и превращает 4 секунды в 80 мс. С безопасностью то же самое: команда check --deploy за секунду находит включённый DEBUG и отсутствие HTTPS-настроек — то, что в проде означает утечку данных. Эти инструменты экономят дни отладки и предотвращают инциденты, которые иначе обнаружились бы по факту взлома или жалоб на тормоза.

django-debug-toolbar: видеть, что делает страница

Главный инструмент диагностики производительности на разработке — django-debug-toolbar. Это панель сбоку страницы, которая для каждого запроса показывает: сколько SQL-запросов ушло в БД, сколько каждый занял, какие были дубли, время рендера шаблона, использованный кэш. Ставится в dev-зависимости и подключается только при DEBUG=True.

# settings.py (только для разработки!)
if DEBUG:
    INSTALLED_APPS += ["debug_toolbar"]
    MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
    INTERNAL_IPS = ["127.0.0.1"]

Самое ценное — панель SQL. Она прямо подсвечивает дублирующиеся запросы: если видите «312 queries, 300 similar», вы поймали N+1. Toolbar не угадывает — он показывает факты, и оптимизировать становится не «по наитию», а по числам: вот этот блок шаблона стоит 300 запросов, уберём его в кэш или поправим ORM. Категорически важно: toolbar включают только на разработке. На проде он раскрывает структуру запросов и внутренности — это и дыра в безопасности, и тормоз.

Оптимизация запросов: N+1 и тяжёлые выборки

Самая частая беда производительности Django — проблема N+1. Она возникает, когда вы перебираете объекты и для каждого лезете за связанной записью. ORM ленив: order.customer внутри цикла — это отдельный запрос на каждой итерации.

# N+1: 1 запрос за заказами + по запросу за customer на КАЖДЫЙ заказ
for order in Order.objects.all():       # 1 запрос
    print(order.customer.name)          # +1 запрос на каждой итерации — N+1

# Фикс для ForeignKey/OneToOne: select_related делает JOIN, один запрос
for order in Order.objects.select_related("customer"):
    print(order.customer.name)          # данные customer уже подгружены

# Фикс для ManyToMany/обратных связей: prefetch_related — второй запрос разом
for product in Product.objects.prefetch_related("tags"):
    print([t.name for t in product.tags.all()])  # теги взяты одним доп. запросом

Правило простое: select_related для «один ко многим» вперёд (ForeignKey, OneToOne) — он добавляет JOIN; prefetch_related для «многие ко многим» и обратных связей — он делает второй запрос и сшивает данные в Python. Кроме N+1 на проде помогают: only("field1", "field2") — забрать из БД только нужные поля вместо всех; values()/values_list() — получить словари/кортежи вместо тяжёлых объектов; .iterator() — обходить огромную выборку, не загружая её целиком в память; и индексы в БД на поля, по которым часто фильтруют и сортируют.

ПриёмКогда применять
select_relatedForeignKey / OneToOne — подгрузить связь одним JOIN
prefetch_relatedManyToMany и обратные связи — добрать вторым запросом
only / deferтаблица широкая, а нужно несколько полей
.iterator()обход очень большой выборки без загрузки в память
индекс в БДчастый фильтр/сортировка по полю — ускоряет выборку

manage.py check --deploy: автоматический аудит

Перед выкаткой запускают встроенную проверку настроек на безопасность. Команда check --deploy прогоняет набор правил и говорит, что в конфигурации опасно для прода.

python manage.py check --deploy

Типичные предупреждения, которые она выдаёт, и что они значат:

ПредупреждениеЧто включить в проде
DEBUG включёнDEBUG = False — иначе трассировки с кодом видны всем
нет SECURE_SSL_REDIRECTпринудительный редирект HTTP → HTTPS
cookies без SecureSESSION_COOKIE_SECURE = True, CSRF_COOKIE_SECURE = True
нет HSTSSECURE_HSTS_SECONDS — браузер запомнит ходить только по HTTPS
пустой ALLOWED_HOSTSперечислить домены, с которых принимаем запросы

Это не панацея, но дешёвый и обязательный шаг: за секунду команда ловит самые грубые промахи конфигурации. Её разумно встроить в CI, чтобы деплой с DEBUG=True вообще не проходил.

Оборонительный чеклист безопасности

Django защищает от большинства типовых атак по умолчанию — важно не отключить эту защиту и не обойти её случайно. Разберём оборону по трём фронтам.

CSRF — подделка межсайтовых запросов

Атака: чужой сайт от имени залогиненного пользователя шлёт POST на ваш (например, «перевести деньги»). Django защищает токеном: каждая форма получает скрытый CSRF-токен, и запрос без верного токена отклоняется. От вас требуется немного — не выключать CsrfViewMiddleware и ставить {% raw %}{% csrf_token %}{% endraw %} в каждую форму.

{% raw %}<form method="post">
  {% csrf_token %}   {# обязателен — без него POST отклонится #}
  ...
</form>{% endraw %}

Главная ошибка — увидеть ошибку «CSRF verification failed» и «починить» её декоратором @csrf_exempt. Это не починка, а снятие защиты: так оставляют дыру. Правильно — разобраться, почему токен не дошёл (обычно забыт тег в форме или AJAX не шлёт заголовок).

XSS — внедрение чужого скрипта

Атака: пользователь вводит <script>...</script> в комментарий, и он исполняется в браузере других. Шаблонизатор Django автоматически экранирует весь вывод переменных: {% raw %}{{ comment }}{% endraw %} превратит < в &lt;, и скрипт станет безобидным текстом. Опасность появляется, когда экранирование отключают фильтром |safe или тегом autoescape off на недоверенных данных.

{% raw %}{{ user_comment }}          {# безопасно: Django экранирует автоматически #}
{{ user_comment|safe }}     {# ОПАСНО на пользовательском вводе: скрипт исполнится #}{% endraw %}

Правило: |safe применяйте только к HTML, которому доверяете (например, отрендеренному вами из Markdown с санитайзером), и никогда — к сырому вводу пользователя.

Секреты — ключи и пароли

В коде не должно быть секретов: SECRET_KEY, пароль БД, ключи внешних API. Попав в git, они навсегда остаются в истории и утекают вместе с репозиторием. Секреты держат в переменных окружения и читают из них.

# settings.py — секрет НЕ в коде, а из окружения
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]  # упадёт, если забыли задать — и хорошо
DEBUG = os.environ.get("DJANGO_DEBUG", "") == "1"
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")

Файл с реальными значениями (.env, prod.env) добавляют в .gitignore, а в репозитории держат только пример с пустыми полями. И ещё две опоры обороны: пароли пользователей Django хранит хешированными (PBKDF2) — никогда не храните их в открытом виде; SQL-инъекции ORM предотвращает сам, пока вы пользуетесь ORM, а не склеиваете сырой SQL из пользовательского ввода в .raw().

Как это работает под капотом

Защиты Django — это в основном middleware, обрабатывающее каждый запрос и ответ. CsrfViewMiddleware на входе сверяет токен из формы с токеном в куке (double-submit cookie), и несовпадение отклоняет запрос ещё до вьюхи. SecurityMiddleware на выходе навешивает защитные HTTP-заголовки (HSTS, редирект на HTTPS) согласно настройкам SECURE_*. Автоэкранирование XSS встроено в шаблонизатор: при выводе переменной он прогоняет её через escape(), заменяя < > & " ' на HTML-сущности, — а |safe лишь помечает строку как «уже безопасную», отключая этот шаг. Команда check --deploy — это набор зарегистрированных функций-проверок (system checks), каждая смотрит на конкретную настройку и при риске возвращает предупреждение с кодом; тот же механизм system checks Django использует и для проверки моделей при старте. Понимание, что всё это слои конвейера запрос-ответ, объясняет, почему «отключить middleware» или «обернуть в csrf_exempt» так опасно: вы вынимаете звено из защитной цепи, через которое проходит каждый запрос.

Частые ошибки

  • Оставить DEBUG=True в проде. Любая ошибка покажет трассировку с фрагментами кода и настройками — подарок атакующему. check --deploy ловит это первым.
  • «Лечить» CSRF через @csrf_exempt. Это не исправление, а снятие защиты. Причина почти всегда — забытый {% raw %}{% csrf_token %}{% endraw %} или AJAX без заголовка.
  • Применять |safe к пользовательскому вводу. Снимает XSS-экранирование и впускает чужой скрипт. |safe — только для доверенного, уже санитизированного HTML.
  • Игнорировать дубли запросов в toolbar. «300 similar queries» — это N+1, который чинится select_related/prefetch_related, а не более мощным сервером.
  • Хранить секреты в коде/гите. Ключи и пароли в репозитории утекают навсегда и остаются в истории даже после удаления. Только переменные окружения, файл с ними — в .gitignore.

Итоги

  • django-debug-toolbar на разработке показывает число и дубли SQL-запросов — главный инструмент поиска узких мест; в проде его держать нельзя.
  • Проблема N+1 чинится select_related (ForeignKey/OneToOne, JOIN) и prefetch_related (ManyToMany/обратные, второй запрос); для прода полезны only, values, .iterator() и индексы.
  • manage.py check --deploy за секунду находит опасные настройки (DEBUG, отсутствие HTTPS/HSTS, незащищённые cookies) — встраивайте в CI.
  • Django защищает от CSRF (токен в форме), XSS (автоэкранирование шаблона) и SQL-инъекций (ORM) по умолчанию — задача в том, чтобы не отключить защиту (csrf_exempt, |safe, сырой SQL).
  • Секреты — только в переменных окружения, не в коде и не в git; пароли Django хранит хешированными.
Проверьте себя
1. Что обычно означает строка «312 queries, 300 similar» в SQL-панели django-debug-toolbar?
AБаза данных сломана и возвращает ошибки
BПроблема N+1: в цикле для каждого объекта делается отдельный запрос за связанной записью — лечится select_related или prefetch_related
CToolbar работает неправильно и дублирует строки
DНужно увеличить мощность сервера базы данных
2. Что делает команда python manage.py check --deploy?
AРазворачивает приложение на production-сервер
BПрогоняет набор проверок настроек и предупреждает об опасных для прода конфигурациях: включённый DEBUG, отсутствие HTTPS/HSTS, незащищённые cookies
CЗапускает все тесты проекта
DСоздаёт резервную копию базы данных
3. Почему опасно «чинить» ошибку «CSRF verification failed» декоратором @csrf_exempt?
Acsrf_exempt замедляет вьюху
BЭто не исправление, а отключение CSRF-защиты вьюхи — остаётся дыра для подделки межсайтовых запросов; настоящая причина обычно в забытом {% csrf_token %} в форме
Ccsrf_exempt удаляет данные из формы
DПосле csrf_exempt форма перестаёт работать совсем
4. Когда применение фильтра |safe в шаблоне Django создаёт уязвимость XSS?
AВсегда, |safe нельзя использовать никогда
BКогда его применяют к недоверенным данным (например, к вводу пользователя) — отключённое экранирование позволит исполнить вставленный скрипт
CТолько при DEBUG=True
D|safe никак не связан с безопасностью