Учебное пособие по Django, Flask и Redis: управление сеансом веб-приложения между платформами Python

Опубликовано: 2022-03-11

Django против Flask: когда Django — неправильный выбор

Я люблю и использую Django во многих моих личных и клиентских проектах, в основном для более классических веб-приложений и тех, которые связаны с реляционными базами данных. Однако Джанго не серебряная пуля.

По замыслу Django очень тесно связан со своим ORM, системой Template Engine System и объектом Settings. Кроме того, это не новый проект: он несет в себе большой багаж, чтобы оставаться обратно совместимым.

Некоторые разработчики Python считают это серьезной проблемой. Они говорят, что Django недостаточно гибок, и по возможности избегают его, а вместо этого используют микрофреймворк Python, такой как Flask.

Я не разделяю этого мнения. Django великолепен, когда используется в подходящем месте и в нужное время, даже если он не вписывается в каждую спецификацию проекта. Как гласит мантра: «Используйте правильный инструмент для работы».

(Даже если это неподходящее место и время, иногда программирование с помощью Django может иметь уникальные преимущества.)

В некоторых случаях действительно может быть полезно использовать более легкий фреймворк (например, Flask). Часто эти микрофреймворки начинают сиять, когда вы понимаете, насколько легко их взломать.

Микрофреймворки спешат на помощь

В нескольких моих клиентских проектах мы обсуждали отказ от Django и переход на микрофреймворк, как правило, когда клиенты хотят сделать что-то интересное (в одном случае, например, встроить ZeroMQ в объект приложения) и проект цели кажутся более трудными для достижения с Django.

В целом, я нахожу Flask полезным для:

  • Простые серверные части REST API
  • Приложения, которым не требуется доступ к базе данных
  • Веб-приложения на основе NoSQL
  • Веб-приложения с очень специфическими требованиями, такими как настраиваемые конфигурации URL-адресов.

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

Возник вопрос: Django — это сделка по принципу «все или ничего»?

Возник вопрос: Django — это сделка по принципу «все или ничего»? Должны ли мы полностью исключить его из проекта, или мы можем научиться сочетать его с гибкостью других микрофреймворков или традиционных фреймворков? Можем ли мы привередничать и выбирать то, что хотим использовать, и избегать других?

Можем ли мы иметь лучшее из обоих миров? Я говорю «да», особенно когда речь идет об управлении сессиями.

(Не говоря уже о том, что существует множество проектов для фрилансеров Django.)

Теперь учебник по Python: совместное использование сеансов Django

Цель этого поста — делегировать Django задачи аутентификации и регистрации пользователей, но использовать Redis для совместного использования пользовательских сессий с другими фреймворками. Я могу придумать несколько сценариев, в которых что-то подобное было бы полезно:

  • Вам нужно разработать REST API отдельно от вашего приложения Django, но вы хотите поделиться данными сеанса.
  • У вас есть определенный компонент, который, возможно, потребуется заменить позже или масштабировать по какой-либо причине, и ему все еще нужны данные сеанса.

В этом руководстве я буду использовать Redis для совместного использования сеансов между двумя фреймворками (в данном случае Django и Flask). В текущей настройке я буду использовать SQLite для хранения информации о пользователе, но при необходимости вы можете привязать свой сервер к базе данных NoSQL (или альтернативе на основе SQL).

Понимание сеансов

Чтобы разделить сеансы между Django и Flask, нам нужно немного узнать о том, как Django хранит информацию о сеансе. Документы Django довольно хороши, но я приведу некоторые сведения для полноты картины.

Разновидности управления сессиями

Как правило, вы можете управлять данными сеанса вашего приложения Python одним из двух способов:

  • Сеансы на основе файлов cookie . В этом сценарии данные сеанса не сохраняются в хранилище данных на серверной части. Вместо этого он сериализуется, подписывается (с помощью SECRET_KEY) и отправляется клиенту. Когда клиент отправляет эти данные обратно, их целостность проверяется на предмет подделки, и они снова десериализуются на сервере.

  • Сеансы на основе хранилища : в этом сценарии сами данные сеанса не отправляются клиенту. Вместо этого отправляется только небольшая часть (ключ), чтобы указать идентификатор текущего пользователя, хранящийся в хранилище сеансов.

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

Общий рабочий процесс

Общий рабочий процесс обработки и управления сеансом будет похож на эту схему:

Диаграмма, показывающая управление сеансами пользователей между Flask и Django с помощью Redis.

Давайте рассмотрим совместное использование сеансов более подробно:

  1. Когда приходит новый запрос, первым шагом является его отправка через зарегистрированное промежуточное ПО в стеке Django. Здесь нас интересует класс SessionMiddleware , который, как и следовало ожидать, связан с управлением и обработкой сеансов:

     class SessionMiddleware(object): def process_request(self, request): engine = import_module(settings.SESSION_ENGINE) session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None) request.session = engine.SessionStore(session_key)

    В этом фрагменте Django захватывает зарегистрированный SessionEngine (мы скоро доберемся до этого), извлекает SESSION_COOKIE_NAME из request ( по умолчанию sessionid ) и создает новый экземпляр выбранного SessionEngine для обработки хранилища сеансов.

  • Позже (после обработки пользовательского представления, но все еще в стеке промежуточного программного обеспечения) механизм сеанса вызывает свой метод сохранения, чтобы сохранить любые изменения в хранилище данных. (Во время обработки представления пользователь мог изменить несколько вещей в сеансе, например, добавив новое значение в объект сеанса с помощью request.session .) Затем клиенту отправляется SESSION_COOKIE_NAME . Вот упрощенная версия:

     def process_response(self, request, response): .... if response.status_code != 500: request.session.save() response.set_cookie(settings.SESSION_COOKIE_NAME, request.session.session_key, max_age=max_age, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, path=settings.SESSION_COOKIE_PATH, secure=settings.SESSION_COOKIE_SECURE or None, httponly=settings.SESSION_COOKIE_HTTPONLY or None) return response

Нас особенно интересует класс SessionEngine , который мы заменим чем-то для хранения и загрузки данных в серверную часть Redis и из нее.

К счастью, есть несколько проектов, которые уже справляются с этим за нас. Вот пример из redis_sessions_fork . Обратите особое внимание на методы save и load , которые написаны так, чтобы (соответственно) сохранять и загружать сеанс в Redis и из него:

 class SessionStore(SessionBase): """ Redis session back-end for Django """ def __init__(self, session_key=None): super(SessionStore, self).__init__(session_key) def _get_or_create_session_key(self): if self._session_key is None: self._session_key = self._get_new_session_key() return self._session_key def load(self): session_data = backend.get(self.session_key) if not session_data is None: return self.decode(session_data) else: self.create() return {} def exists(self, session_key): return backend.exists(session_key) def create(self): while True: self._session_key = self._get_new_session_key() try: self.save(must_create=True) except CreateError: continue self.modified = True self._session_cache = {} return def save(self, must_create=False): session_key = self._get_or_create_session_key() expire_in = self.get_expiry_age() session_data = self.encode(self._get_session(no_load=must_create)) backend.save(session_key, expire_in, session_data, must_create) def delete(self, session_key=None): if session_key is None: if self.session_key is None: return session_key = self.session_key backend.delete(session_key)

Важно понимать, как работает этот класс, поскольку нам нужно будет реализовать что-то подобное на Flask для загрузки данных сеанса. Давайте подробнее рассмотрим пример REPL:

 >>> from django.conf import settings >>> from django.utils.importlib import import_module >>> engine = import_module(settings.SESSION_ENGINE) >>> engine.SessionStore() <redis_sessions_fork.session.SessionStore object at 0x3761cd0> >>> store["count"] = 1 >>> store.save() >>> store.load() {u'count': 1}

Интерфейс хранилища сессий довольно прост для понимания, но под капотом много всего происходит. Мы должны копнуть немного глубже, чтобы реализовать нечто подобное на Flask.

Примечание. Вы можете спросить: «Почему бы просто не скопировать SessionEngine во Flask?» Проще сказать, чем сделать. Как мы обсуждали в начале, Django тесно связан со своим объектом Settings, поэтому вы не можете просто импортировать какой-нибудь модуль Django и использовать его без каких-либо дополнительных действий.

Сессия Django (де-)сериализация

Как я уже сказал, Django много работает, чтобы замаскировать сложность своего хранилища сессий. Давайте проверим ключ Redis, который хранится в приведенных выше фрагментах:

 >>> store.session_key u"ery3j462ezmmgebbpwjajlxjxmvt5adu"

Теперь давайте запросим этот ключ в redis-cli:

 redis 127.0.0.1:6379> get "django_sessions:ery3j462ezmmgebbpwjajlxjxmvt5adu" "ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ=="

Здесь мы видим очень длинную строку в кодировке Base64. Чтобы понять его назначение, нам нужно взглянуть на класс SessionBase Django, чтобы увидеть, как он обрабатывается:

 class SessionBase(object): """ Base class for all Session classes. """ def encode(self, session_dict): "Returns the given session dictionary serialized and encoded as a string." serialized = self.serializer().dumps(session_dict) hash = self._hash(serialized) return base64.b64encode(hash.encode() + b":" + serialized).decode('ascii') def decode(self, session_data): encoded_data = base64.b64decode(force_bytes(session_data)) try: hash, serialized = encoded_data.split(b':', 1) expected_hash = self._hash(serialized) if not constant_time_compare(hash.decode(), expected_hash): raise SuspiciousSession("Session data corrupted") else: return self.serializer().loads(serialized) except Exception as e: # ValueError, SuspiciousOperation, unpickling exceptions if isinstance(e, SuspiciousOperation): logger = logging.getLogger('django.security.%s' % e.__class__.__name__) logger.warning(force_text(e)) return {}

Метод encode сначала сериализует данные с помощью текущего зарегистрированного сериализатора. Другими словами, он преобразует сеанс в строку, которую позже может преобразовать обратно в сеанс (дополнительную информацию см. в документации по SESSION_SERIALIZER). Затем он хэширует сериализованные данные и позже использует этот хэш в качестве подписи для проверки целостности данных сеанса. Наконец, эта пара данных возвращается пользователю в виде строки в кодировке Base64.

Кстати: до версии 1.6 Django по умолчанию использовала pickle для сериализации данных сеанса. Из соображений безопасности метод сериализации по умолчанию теперь django.contrib.sessions.serializers.JSONSerializer .

Кодирование примера сеанса

Давайте посмотрим на процесс управления сеансом в действии. Здесь наш словарь сеанса будет просто числом и некоторым целым числом, но вы можете себе представить, как это можно обобщить на более сложные пользовательские сеансы.

 >>> store.encode({'count': 1}) u'ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ==' >>> base64.b64decode(encoded) 'fe1964e1d2cf8069d9f1823afd143400b6d3736f:{"count":1}'

Результатом метода store (u'ZmUxOTY…==') является закодированная строка, содержащая сериализованный сеанс пользователя и его хэш. Когда мы его декодируем, мы действительно получаем обратно и хэш ('fe1964e…'), и сеанс ( {"count":1} ).

Обратите внимание, что метод декодирования проверяет правильность хэша для этого сеанса, гарантируя целостность данных, когда мы переходим к их использованию во Flask. В нашем случае мы не слишком беспокоимся о том, что наш сеанс будет подделан на стороне клиента, потому что:

  • Мы не используем сеансы на основе файлов cookie, т. е. мы не отправляем клиенту все пользовательские данные.

  • Во Flask нам понадобится SessionStore только для чтения, который сообщит нам, существует ли данный ключ или нет, и вернет сохраненные данные.

Расширение до колбы

Далее давайте создадим упрощенную версию сессионного движка (базы данных) Redis для работы с Flask. Мы будем использовать тот же SessionStore (определенный выше) в качестве базового класса, но нам нужно будет удалить некоторые его функции, например, проверку на наличие неверных подписей или изменение сеансов. Нас больше интересует SessionStore , доступный только для чтения, который будет загружать данные сеанса, сохраненные из Django. Давайте посмотрим, как это сочетается:

 class SessionStore(object): # The default serializer, for now def __init__(self, conn, session_key, secret, serializer=None): self._conn = conn self.session_key = session_key self._secret = secret self.serializer = serializer or JSONSerializer def load(self): session_data = self._conn.get(self.session_key) if not session_data is None: return self._decode(session_data) else: return {} def exists(self, session_key): return self._conn.exists(session_key) def _decode(self, session_data): """ Decodes the Django session :param session_data: :return: decoded data """ encoded_data = base64.b64decode(force_bytes(session_data)) try: # Could produce ValueError if there is no ':' hash, serialized = encoded_data.split(b':', 1) # In the Django version of that they check for corrupted data # I don't find it useful, so I'm removing it return self.serializer().loads(serialized) except Exception as e: # ValueError, SuspiciousOperation, unpickling exceptions. If any of # these happen, return an empty dictionary (ie, empty session). return {}

Нам нужен только метод load , потому что это реализация хранилища только для чтения. Это означает, что вы не можете выйти из Flask напрямую; вместо этого вы можете перенаправить эту задачу в Django. Помните, что цель здесь — управлять сеансами между этими двумя платформами Python, чтобы дать вам больше гибкости.

Колбочные сеансы

Микрофреймворк Flask поддерживает сеансы на основе файлов cookie, что означает, что все данные сеанса отправляются клиенту в кодировке Base64 и с криптографической подписью. Но на самом деле нас не очень интересует поддержка сессий Flask.

Что нам нужно, так это получить идентификатор сеанса, созданный Django, и сравнить его с серверной частью Redis, чтобы мы могли быть уверены, что запрос принадлежит предварительно подписанному пользователю. Таким образом, идеальный процесс был бы таким (это совпадает с диаграммой выше):

  • Мы получаем идентификатор сеанса Django из файла cookie пользователя.
  • Если идентификатор сеанса найден в Redis, мы возвращаем сеанс, соответствующий этому идентификатору.
  • Если нет, мы перенаправляем их на страницу входа.

Будет удобно иметь декоратор для проверки этой информации и установки текущего user_id в переменную g в Flask:

 from functools import wraps from flask import g, request, redirect, url_for def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): djsession_id = request.cookies.get("sessionid") if djsession_id is None: return redirect("/") key = get_session_prefixed(djsession_id) session_store = SessionStore(redis_conn, key) auth = session_store.load() if not auth: return redirect("/") g.user_id = str(auth.get("_auth_user_id")) return f(*args, **kwargs) return decorated_function

В приведенном выше примере мы все еще используем SessionStore , который мы определили ранее, для получения данных Django из Redis. Если сессия имеет _auth_user_id , мы возвращаем контент из функции просмотра; в противном случае пользователь перенаправляется на страницу входа, как мы и хотели.

Склеивание вещей вместе

Для обмена файлами cookie мне удобно запускать Django и Flask через сервер WSGI и склеивать их вместе. В этом примере я использовал CherryPy:

 from app import app from django.core.wsgi import get_wsgi_application application = get_wsgi_application() d = wsgiserver.WSGIPathInfoDispatcher({ "/":application, "/backend":app }) server = wsgiserver.CherryPyWSGIServer(("127.0.0.1", 8080), d)

При этом Django будет обслуживаться на «/», а Flask будет обслуживаться на конечных точках «/ backend».

В заключение

Вместо того, чтобы сравнивать Django и Flask или рекомендовать вам изучить только микрофреймворк Flask, я объединил Django и Flask, заставив их использовать одни и те же данные сеанса для аутентификации, делегировав задачу Django. Поскольку Django поставляется с множеством модулей для регистрации пользователей, входа в систему и выхода из системы (и это лишь некоторые из них), объединение этих двух фреймворков сэкономит вам драгоценное время, а также предоставит вам возможность взломать управляемый микрофреймворк, такой как Flask.