Производительность и безопасность
Рабочее приложение и приложение, готовое к проду, — разные вещи: первое отвечает на запросы, второе делает это быстро и не отдаёт лишнего злоумышленнику. Этот урок — про инструменты диагностики и оборонительный чеклист перед выкаткой.
Готовность к продакшену — это сочетание производительности (приложение держит нагрузку, не делая лишней работы) и безопасности (оно защищено от типовых атак и не раскрывает секреты). 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_related | ForeignKey / OneToOne — подгрузить связь одним JOIN |
prefetch_related | ManyToMany и обратные связи — добрать вторым запросом |
only / defer | таблица широкая, а нужно несколько полей |
.iterator() | обход очень большой выборки без загрузки в память |
| индекс в БД | частый фильтр/сортировка по полю — ускоряет выборку |
manage.py check --deploy: автоматический аудит
Перед выкаткой запускают встроенную проверку настроек на безопасность. Команда check --deploy прогоняет набор правил и говорит, что в конфигурации опасно для прода.
python manage.py check --deploy
Типичные предупреждения, которые она выдаёт, и что они значат:
| Предупреждение | Что включить в проде |
| DEBUG включён | DEBUG = False — иначе трассировки с кодом видны всем |
| нет SECURE_SSL_REDIRECT | принудительный редирект HTTP → HTTPS |
| cookies без Secure | SESSION_COOKIE_SECURE = True, CSRF_COOKIE_SECURE = True |
| нет HSTS | SECURE_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 %} превратит < в <, и скрипт станет безобидным текстом. Опасность появляется, когда экранирование отключают фильтром |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 хранит хешированными.