Создание middleware, порядок выполнения, request/response hooks
Middleware — слой вокруг маршрутизации и view для поведения, которое действительно относится ко многим запросам: security headers, session/auth plumbing, correlation id, locale. Правило публикации поста или расчёт тарифа к middleware не относятся.
Материал актуален для Django 5.2 LTS и 6.0. Встроенная CSP отдельно помечена как новинка Django 6.0.
При списке [A, B, C] запрос входит как A → B → C → view, а ответ выходит view → C → B → A. Каждый слой решает, вызывать ли следующий:
request → A → B → C → view
response ← A ← B ← C ←────┘Если B вернул response до get_response(), C и view не выполнятся, а ответ пройдёт наружу через A. Поэтому middleware ниже по списку не гарантированно увидит каждый запрос.
Порядок — часть корректности. SessionMiddleware должен идти до AuthenticationMiddleware, а MessageMiddleware зависит от sessions. Response-фаза идёт в обратном направлении, что важно для cache, compression и заголовков.
from time import monotonic
class TimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
started_at = monotonic()
response = self.get_response(request)
response.headers["Server-Timing"] = (
f"app;dur={(monotonic() - started_at) * 1000:.1f}"
)
return responseЭкземпляр middleware создаётся один раз при старте процесса и обслуживает много запросов. Не храните текущий request, user или timer в self: при параллельных запросах значения смешаются. Локальная переменная и атрибут самого request принадлежат запросу.
Server-Timing может раскрывать внутренние детали; публикуйте только согласованные метрики или включайте заголовок в диагностической среде.
Factory/callable можно дополнить методами:
process_view(request, view_func, view_args, view_kwargs) — перед вызовом найденной view;process_exception(request, exception) — если view выбросила исключение;process_template_response(request, response) — для ответа с методом render().Hook возвращает None, чтобы продолжить, либо HttpResponse, чтобы подменить дальнейшую обработку. Exception hooks вызываются в обратном порядке middleware; если один вернул response, остальные получают уже response-фазу.
MiddlewareMixin адаптирует старые process_request/process_response классы и не удалён. Он полезен при сопровождении legacy, но новый простой слой легче написать callable-стилем.
Не пытайтесь ловить все исключения обычным try/except вокруг self.get_response(): Django преобразует исключения в responses на границах цепочки. Для site-wide 400/403/404/500 используйте URL handlers и error templates, для DRF — его exception handler, а process_exception оставляйте для действительно общего поведения.
Типовой проект:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]Это разумная основа, не универсальная формула для любого стороннего слоя. Читайте раздел middleware ordering документации каждого компонента. Например, compression должен оборачивать responses в правильном месте относительно conditional GET/cache.
SecurityMiddleware управляет HTTPS redirect, HSTS, X-Content-Type-Options, Referrer Policy и Cross-Origin Opener Policy согласно settings.SessionMiddleware добавляет request.session и сохраняет изменённую сессию в response.CommonMiddleware реализует APPEND_SLASH, PREPEND_WWW и некоторые общие проверки.CsrfViewMiddleware проверяет unsafe cookie-authenticated requests.AuthenticationMiddleware добавляет лениво вычисляемый request.user и зависит от sessions.MessageMiddleware связывает messages с request/response.XFrameOptionsMiddleware добавляет защиту от framing по X_FRAME_OPTIONS.CommonMiddleware не сжимает response и не является текущим механизмом ETag. Для сжатия есть GZipMiddleware/reverse proxy, для conditional responses — ConditionalGetMiddleware и HTTP cache semantics.
SECURE_SSL_REDIRECT = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
SECURE_CROSS_ORIGIN_OPENER_POLICY = "same-origin"
# HSTS сначала вводят с малым значением и только на полностью HTTPS-домене.
SECURE_HSTS_SECONDS = 300
SECURE_HSTS_INCLUDE_SUBDOMAINS = False
SECURE_HSTS_PRELOAD = FalseHSTS браузер помнит вне приложения. Год, includeSubDomains и preload нельзя включать, пока каждый поддомен не готов к HTTPS и нет плана отката.
За TLS-terminating proxy настройте SECURE_PROXY_SSL_HEADER только если proxy удаляет входящий header клиента и сам выставляет проверенное значение. Иначе злоумышленник может подделать request.is_secure(), что влияет на redirect и CSRF.
SECURE_BROWSER_XSS_FILTER удалён в Django 4.0: устаревший browser XSS auditor не заменял CSP. USE_ETAGS также удалён в Django 4.0. Не переносите эти settings из старого чеклиста.
Для server-rendered формы:
<form method="post">
{% csrf_token %}
{{ form.as_div }}
<button type="submit">Сохранить</button>
</form>Middleware защищает unsafe методы (POST, PUT, PATCH, DELETE), когда используется cookie/session authentication. CSRF_TRUSTED_ORIGINS разрешает проверенные origins на origin/referer-этапе, но не отменяет CSRF token. CORS и CSRF — разные механизмы.
csrf_exempt применяют только после анализа модели аутентификации. Endpoint с bearer token, недоступным браузеру как ambient cookie, может не нуждаться в CSRF; session-auth API нуждается. Глобальное исключение /api/ по prefix опасно.
import logging
from time import monotonic
from uuid import uuid4
logger = logging.getLogger(__name__)
class RequestLogMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.request_id = uuid4().hex
started_at = monotonic()
response = self.get_response(request)
duration_ms = (monotonic() - started_at) * 1000
logger.info(
"http_request_finished",
extra={
"request_id": request.request_id,
"method": request.method,
"path": request.path,
"status_code": response.status_code,
"duration_ms": round(duration_ms, 1),
},
)
response.headers["X-Request-ID"] = request.request_id
return responseНе включайте query string, cookies, Authorization header, password и body по умолчанию. Если инфраструктура уже выдаёт trace/request id, доверяйте входящему значению только от контролируемого proxy и валидируйте формат/длину. Для distributed tracing лучше стандарт W3C Trace Context через observability SDK.
Если нужно логировать 500 при исключении, используйте настроенный Django error logger/APM: показанный response-only слой может увидеть статус, но не само уже преобразованное исключение.
Translation state контекстный, и его надо восстановить даже при исключении:
from django.utils import translation
class DomainLocaleMiddleware:
languages = {
"example.com": "en",
"example.ru": "ru",
}
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
host = request.get_host().partition(":")[0]
language = self.languages.get(host, "en")
request.LANGUAGE_CODE = language
with translation.override(language):
return self.get_response(request)request.get_host() проверяет ALLOWED_HOSTS; чтение сырого Host из META обходит эту защиту. Для стандартного выбора языка сначала рассмотрите LocaleMiddleware.
cache.get() + set()Наивный middleware обычно ошибается сразу в нескольких местах:
set(..., timeout=60) двигает окно;X-Forwarded-For подделывается без доверенной proxy-chain;Для внешнего трафика предпочтителен rate limit на CDN/API gateway/reverse proxy. Прикладной limiter строят на общей atomic storage (например, Redis script), ключе actor/API key плюс проверенный network signal, именованной политике endpoint и ответе 429 с Retry-After. Fail-open/fail-closed выбирают по риску операции.
User-Agent нельзя считать identity или security signal: клиент меняет его одной строкой. Блокировка известных crawler может уменьшить шум, но не является защитой.
Надёжнее переключать maintenance на load balancer/CDN, оставляя health endpoint для оркестратора. Django middleware требует осторожного порядка: если он обращается к request.user, то должен стоять после AuthenticationMiddleware; исключения по /admin/, /static/, /media/ легко раскрывают лишние пути.
Если режим реализован в приложении, используйте именованную политику/список разрешённых routes, возвращайте 503 и Retry-After, не кэшируйте персональный ответ и тестируйте staff bypass. Флаг должен быть общим для всех workers, не модульной переменной.
from django.utils.csp import CSP
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.middleware.csp.ContentSecurityPolicyMiddleware",
# остальные middleware
]
SECURE_CSP_REPORT_ONLY = {
"default-src": [CSP.SELF],
"script-src": [CSP.SELF, CSP.NONCE],
"style-src": [CSP.SELF],
"img-src": [CSP.SELF, "data:", "https:"],
"frame-ancestors": [CSP.NONE],
"report-uri": ["/csp-reports/"],
}Начните с report-only, устраните inline scripts/styles или переведите нужные scripts на nonce, затем включайте SECURE_CSP. Для nonce нужен django.template.context_processors.csp и применение {{ csp_nonce }} согласно CSP how-to.
Django строит header, но не проверяет смысл и валидность всех directives. Report endpoint сам принимает недоверенный JSON, требует ограничения размера/rate и не должен отражать данные в HTML.
В Django 5.2 встроенного CSP middleware нет: policy добавляет reverse proxy или поддерживаемая сторонняя библиотека. Не копируйте unsafe-inline ради того, чтобы «CSP не ломал сайт»: это существенно ослабляет script policy.
COEP/CORP/COOP не включают комплектом без теста. Cross-Origin-Embedder-Policy: require-corp, например, блокирует ресурсы, которые не дают подходящий CORS/CORP response.
Middleware может быть sync-only, async-only или поддерживать оба режима. При несовпадении Django адаптирует вызов, но переходы sync↔async уменьшают выгоду ASGI. Долгая синхронная цепочка может удерживать thread на запрос.
При написании dual-capable middleware следуйте официальному шаблону с sync_capable, async_capable, iscoroutinefunction и markcoroutinefunction; простого async def __call__ недостаточно для универсальной совместимости. Отдельно тестируйте WSGI и ASGI конфигурации.
Проверка request.path.startswith() связывает middleware с текущим prefix и ломается при namespace/versioning. Если поведение не глобально, сузьте его ближе к endpoint.
MiddlewareMixin и hooks не удалены и помогают поддерживать старые пакеты. Удалены SECURE_BROWSER_XSS_FILTER и USE_ETAGS в Django 4.0. Утверждение «CommonMiddleware делает gzip и ETag» для актуального Django неверно.
В Django 6.0 появился встроенный CSP. При одновременном старом custom CSP middleware убедитесь, что response не получает конфликтующие headers, затем мигрируйте поэтапно.
Далее: Signals