Учебное пособие по Django, Flask и Redis: управление сеансом веб-приложения между платформами Python
Опубликовано: 2022-03-11Django против 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.)
Теперь учебник по Python: совместное использование сеансов Django
Цель этого поста — делегировать Django задачи аутентификации и регистрации пользователей, но использовать Redis для совместного использования пользовательских сессий с другими фреймворками. Я могу придумать несколько сценариев, в которых что-то подобное было бы полезно:
- Вам нужно разработать REST API отдельно от вашего приложения Django, но вы хотите поделиться данными сеанса.
- У вас есть определенный компонент, который, возможно, потребуется заменить позже или масштабировать по какой-либо причине, и ему все еще нужны данные сеанса.
В этом руководстве я буду использовать Redis для совместного использования сеансов между двумя фреймворками (в данном случае Django и Flask). В текущей настройке я буду использовать SQLite для хранения информации о пользователе, но при необходимости вы можете привязать свой сервер к базе данных NoSQL (или альтернативе на основе SQL).
Понимание сеансов
Чтобы разделить сеансы между Django и Flask, нам нужно немного узнать о том, как Django хранит информацию о сеансе. Документы Django довольно хороши, но я приведу некоторые сведения для полноты картины.
Разновидности управления сессиями
Как правило, вы можете управлять данными сеанса вашего приложения Python одним из двух способов:
Сеансы на основе файлов cookie . В этом сценарии данные сеанса не сохраняются в хранилище данных на серверной части. Вместо этого он сериализуется, подписывается (с помощью SECRET_KEY) и отправляется клиенту. Когда клиент отправляет эти данные обратно, их целостность проверяется на предмет подделки, и они снова десериализуются на сервере.
Сеансы на основе хранилища : в этом сценарии сами данные сеанса не отправляются клиенту. Вместо этого отправляется только небольшая часть (ключ), чтобы указать идентификатор текущего пользователя, хранящийся в хранилище сеансов.
В нашем примере нас больше интересует последний сценарий: мы хотим, чтобы данные нашего сеанса хранились на сервере, а затем проверялись во Flask. То же самое можно было бы сделать и в первом случае, но, как упоминается в документации Django, есть некоторые опасения по поводу безопасности первого метода.
Общий рабочий процесс
Общий рабочий процесс обработки и управления сеансом будет похож на эту схему:
Давайте рассмотрим совместное использование сеансов более подробно:
Когда приходит новый запрос, первым шагом является его отправка через зарегистрированное промежуточное ПО в стеке 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.