Django、Flask 和 Redis 教程:Python 框架之间的 Web 应用程序会话管理

已发表: 2022-03-11

Django 与 Flask:当 Django 是错误的选择时

我喜欢并在我的许多个人和客户项目中使用 Django,主要用于更经典的 Web 应用程序和那些涉及关系数据库的应用程序。 然而,Django 并不是灵丹妙药。

按照设计,Django 与其 ORM、模板引擎系统和设置对象紧密耦合。 另外,这不是一个新项目:它承载了很多包袱来保持向后兼容。

一些 Python 开发人员认为这是一个主要问题。 他们说 Django 不够灵活,尽可能避免使用它,而是使用像 Flask 这样的 Python 微框架。

我不同意这种观点。 在适当的地点和时间使用 Django 非常棒,即使它不适合每个项目规范。 正如口头禅所说:“为工作使用正确的工具”。

(即使不是正确的地点和时间,有时使用 Django 编程也会有独特的好处。)

在某些情况下,使用更轻量级的框架(如 Flask)确实会很好。 通常,当您意识到这些微框架很容易被破解时,它们就会开始发光。

微框架的救援

在我的一些客户项目中,我们讨论过放弃 Django 并转向微框架,通常是当客户想做一些有趣的事情时(例如,在应用程序对象中嵌入 ZeroMQ)和项目使用 Django 似乎更难以实现目标。

更一般地说,我发现 Flask 可用于:

  • 简单的 REST API 后端
  • 不需要数据库访问的应用程序
  • 基于 NoSQL 的 Web 应用程序
  • 具有非常特殊要求的 Web 应用程序,例如自定义 URL 配置

同时,我们的应用程序需要用户注册和 Django 多年前解决的其他常见任务。 鉴于其重量轻,Flask 没有配备相同的工具包。

问题出现了:Django 是一个孤注一掷的交易吗?

问题出现了:Django 是一个孤注一掷的交易吗? 我们应该将它完全从项目中删除,还是我们可以学习将它与其他微框架或传统框架的灵活性结合起来? 我们可以挑选我们想要使用的部分并避开其他部分吗?

我们可以两全其美吗? 我说是的,尤其是在会话管理方面。

(更不用说,有很多针对 Django 自由职业者的项目。)

现在是 Python 教程:共享 Django 会话

这篇文章的目标是将用户身份验证和注册的任务委托给 Django,同时使用 Redis 与其他框架共享用户会话。 我可以想到一些这样的场景会很有用:

  • 您需要与 Django 应用程序分开开发 REST API,但希望共享会话数据。
  • 您有一个特定组件可能需要稍后更换或出于某种原因向外扩展,并且仍需要会话数据。

在本教程中,我将使用 Redis 在两个框架(在本例中为 Django 和 Flask)之间共享会话。 在当前设置中,我将使用 SQLite 存储用户信息,但如果需要,您可以将后端绑定到 NoSQL 数据库(或基于 SQL 的替代方案)。

了解会话

要在 Django 和 Flask 之间共享会话,我们需要了解一下 Django 是如何存储其会话信息的。 Django 文档非常好,但我将提供一些完整的背景知识。

会话管理品种

通常,您可以选择通过以下两种方式之一来管理 Python 应用程序的会话数据:

  • 基于 Cookie 的会话:在这种情况下,会话数据不存储在后端的数据存储中。 相反,它被序列化、签名(使用 SECRET_KEY)并发送给客户端。 当客户端发回该数据时,会检查其完整性是否被篡改,并在服务器上再次对其进行反序列化。

  • 基于存储的会话:在这种情况下,会话数据本身不会发送到客户端。 相反,只发送一小部分(密钥)来指示当前用户的身份,存储在会话存储中。

在我们的示例中,我们对后一种情况更感兴趣:我们希望将会话数据存储在后端,然后在 Flask 中检查。 前者可以做同样的事情,但正如 Django 文档所述,第一种方法的安全性存在一些问题。

一般工作流程

会话处理和管理的一般工作流程将类似于此图:

显示使用 Redis 管理 Flask 和 Django 之间的用户会话的图表。

让我们更详细地了解会话共享:

  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 (我们将很快了解),从request (默认为sessionid )中提取SESSION_COOKIE_NAME并创建所选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的一个例子。 密切注意saveload方法,它们的编写目的是(分别)将会话存储到 Redis 和从 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 编码字符串。 要了解它的用途,我们需要查看 Django 的SessionBase类来了解它是如何处理的:

 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} )。

请注意,decode 方法会检查以确保该会话的哈希是正确的,从而在我们在 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 创建的会话 ID 并与 Redis 后端进行检查,以便我们可以确定请求属于预签名用户。 总之,理想的过程是(这与上图同步):

  • 我们从用户的 cookie 中获取 Django 会话 ID。
  • 如果在 Redis 中找到会话 ID,我们将返回与该 ID 匹配的会话。
  • 如果没有,我们会将它们重定向到登录页面。

有一个装饰器来检查该信息并将当前user_id设置到 Flask 中的g变量中会很方便:

 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从 Redis 获取 Django 数据。 如果会话有_auth_user_id ,我们从视图函数返回内容; 否则,用户将被重定向到登录页面,就像我们想要的那样。

把东西粘在一起

为了共享 cookie,我发现通过 WSGI 服务器启动 Django 和 Flask 并将它们粘合在一起很方便。 在这个例子中,我使用了 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 这样可管理的微框架的机会。