Программы
Как сделать свою middleware в Django (с примерами)

Как сделать свою middleware в Django (с примерами)

Middleware или "промежуточное программное обеспечение" - элегантный способ установить общие правила обработки запросов и ответов приложения. Давайте напишем парочку middleware, чтобы понять, как они работают.

Во многих фреймворках есть возможность создавать своеобразные прослойки для того, чтобы дополнять запросы к приложению, либо же модифицировать ответы от него. Это middleware. Они работают незаметно для основного приложения, но несут большую пользу. Сами посмотрите:

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, которые подключаются при генерации проекта на Django. Найти и модифицировать список используемых "прослоек" можно в файле настроек settings.py. Что же они дают проекту?

django.middleware.security.SecurityMiddleware проводит ряд проверок запроса к приложению на предмет различных атак:

  • Устанавливает заголовок Strict-Transport-Security, который предписывает браузеру подключаться только по защищённому соединению (HTTPS).
  • Добавляет заголовок X-Content-Type-Options: nosniff, чтобы браузеры не пытались угадать Content-Type переданного файла. Да, это может быть полезно, если сервер настроен неверно, но также "игра в угадайку" может быть использована для атаки.
  • Включает X-XSS-Protection для того, чтобы браузер проверял, GET и POST параметры на предмет JavaScript, дабы предотвратить XSS-атаку.
  • При наличии HTTP и HTTPS версий сайта, перенаправляет на HTTPS версию.

django.contrib.sessions.middleware.SessionMiddleware включает поддержку механизма сессий в Django-проекте. То есть по Session ID, который передаётся в Cookies, находит данные сессии в бекенде для хранения сессий (например, в базе данных).

django.middleware.common.CommonMiddleware добавляет несколько полезных возможностей. Таких как добавление слеша (/) в конце URL, www к домену - если включены соответствующие настройки, поддержка механизма HTTP ETag.

django.middleware.csrf.CsrfViewMiddleware проверяет запросы с данными от клиента на наличие CSRF-токена, дабы предотвратить подделку запроса.

django.contrib.auth.middleware.AuthenticationMiddleware находит по сессии Django-пользователя, подставляя его в поле user объекта request.

django.contrib.messages.middleware.MessageMiddleware позволяет хранить в сессии пользователя небольшие информационные сообщения, которые будут храниться между запросами.

django.middleware.clickjacking.XFrameOptionsMiddleware добавляет защиту от clickjacking-а - подмены сайта посредством frame-элементов.

Таким образом, сложно переоценить пользу от этих маленьких прослоек - middleware.

Изображение Python 3.11. Что нового?

Как же работают middleware?

Возможно, вы знакомы с тем, как работают web-proxy или же Man-in-the-middle. А может быть, вам будет более близка аналогия с декораторами.

В любом случае, middleware встаёт перед приложением, предоставляя интерфейс как у приложения. Таким образом она как бы подменяет его. Благодаря этому middleware получает управление и запрос ещё до приложения, может модифицировать его, провести подготовительные действия.

После этого промежуточное программное обеспечение вызывает само приложение либо же следующую middleware, ожидая ответ.

Получив ответ, middleware может его модифицировать и передать дальше - клиенту или же предыдущей "прослойке".

Получается своеобразная матрёшка или лук - приложение обёрнуто в миддлварь, которая обёрнута в другую и т.д.

Общий вид Django middleware

Как и декораторы в Python, промежуточное программное обеспечение также выглядит как вызываемый объект, который возвращает вызываемый объект. Как вариант - функция, которая возвращает функцию. Внешняя функция принимает параметром функцию для вызова приложения, либо же следующей middleware (get_response), а вложенная функция - запрос к серверу (request).

from typing import Callable
from django.http import HttpRequest, HttpResponse


def name(get_response: Callable[[HttpRequest], HttpResponse]) -> Callable:
    def middleware(request: HttpRequest) -> HttpResponse:
        response = get_response(request)
        return response

    return middleware

Также нужно добавить данную функцию в список middleware в настройках приложения:

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',

    'app_name.module.name',
]

После чего она будет использована и вызвана после прочих стандартных для Django мидлварей.

Пример Django middleware, которая работает с запросом

Предположим, мы хотим сделать так, чтобы пользователи, вошедшие на наш сайт автоматически разлогинивались через некоторое время. В некоторых организациях есть подобные требования безопасности.

Тогда для решения данной задачи нам идеально подойдёт middleware.

Данную middleware можно расположить в приложении для работы с пользователями (в данном случае - user), в модуле middleware:

from datetime import datetime, timedelta
from typing import Callable

from django.conf import settings
from django.contrib.auth import logout
from django.http import HttpRequest, HttpResponse


def logout_on_timeout(get_response: Callable[[HttpRequest], HttpResponse]) -> Callable:
    ttl = settings.LOGOUT_TIMEOUT

    def middleware(request: HttpRequest) -> HttpResponse:
        user = request.user

        if (
            not user.is_anonymous
            and user.last_login < datetime.now() - timedelta(seconds=ttl)
        ):
            logout(request)

        response = get_response(request)
        return response

    return middleware

После получения управления и запроса мы берём из запроса пользователя, поэтому нужно будет подключить данную "прослойку" после django.contrib.auth.middleware.AuthenticationMiddleware, чтобы пользователь уже был инициализирован и находился в request.

Далее проверяем, что пользователь не анонимный (его разлогинивать незачем), а также, что не истекло время работы пользователя (отсчитываем от времени последнего входа пользователя). Если время истекло - разлогиниваем.

Время, которое отпущено пользователю до разлогина указываем в настройках:

LOGOUT_TIMEOUT = 20 * 60  # 20 минут

Не забываем в настройках добавить нашу мидлварь в список MIDDLEWARE.

Пример Django middleware, которая работает с ответом приложения

Возможно, вы обращали внимание, что код html-страниц, сгенерированный вашим приложением, не отличается лаконичностью: куча лишних пробелов, пустых строк и т.д. Таким образом по сети отправляются лишние данные, что не может радовать.

Напишем middleware, которая будет убирать лишние пробелы из начала строк, а также лишние пустые строки.

import re
from django.http import HttpRequest, HttpResponse

RE_EMPTY_STRING = r'\n\s*?\n'
RE_STRING_SPACE_PREFIX = r'\n\s+'


def html_optimize(get_response):
    def middleware(request: HttpRequest):
        response: HttpResponse = get_response(request)

        new_content = re.sub(RE_EMPTY_STRING, lambda *_: '\n', response.content.decode())
        new_content = re.sub(RE_STRING_SPACE_PREFIX, lambda *_: '\n', new_content)
        response.content = new_content.encode()
        response["Content-Length"] = len(response.content)

        return response

    return middleware

Для начала получаем управление и отдаём его дальше, когда всё отработало, мы получаем ответ сервера. Самое время его "почистить"! response.content содержит байты html-страницы. Поэтому декодируем в строку и с помощью регулярных выражений убираем лишние пробелы. Далее - кодируем обратно в байты.

Так как мы изменили содержимое ответа, нужно также изменить заголовок, в котором указан размер содержимого ответа нашего приложения. После чего отправляем объект ответа дальше.

Эта мидлварь не использует ничего из других, поэтому её можно поставить первой в списке MIDDLEWARE в файле настроек нашего Django-приложения.