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

本文概述

  • Django与Flask:当Django是错误的选择时
  • 抢救微框架
  • 现在, Python教程:共享Django会话
  • 了解会话
  • Django会话(反序列化)
  • 扩展到Flask
  • Flask会话
  • 粘合在一起
  • 结论
Django与Flask:当Django是错误的选择时 我在许多个人和客户项目中都喜欢并使用Django, 主要用于更经典的Web应用程序以及涉及关系数据库的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文档所提到的, 第一种方法的安全性令人担忧。
一般工作流程
会话处理和管理的一般工作流程将类似于此图:
Django,Flask和Redis教程(Python框架之间的Web应用程序会话管理)

文章图片
让我们详细了解会话共享:
收到新请求时, 第一步是通过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(我们将很快处理), 从请求(默认为sessionid)中提取SESSION_COOKIE_NAME, 并创建所选SessionEngine的新实例来处理会话存储。
稍后(在处理完用户视图之后, 但仍在中间件堆栈中), 会话引擎调用其save方法将所有更改保存到数据存储中。 (在视图处理期间, 用户可能在会话中进行了一些更改, 例如, 通过向具有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和从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_keydef load(self): session_data = http://www.srcmini.com/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 = {} returndef 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 = http://www.srcmini.com/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}'

存储方法(u’ ZmUxOTY…==’ )的结果是一个编码的字符串, 其中包含序列化的用户会话及其哈希。当我们解码它时, 我们确实会同时获取哈希(‘ fe1964e…’ )和会话({” count” :1})。
注意, 解码方法检查以确保该会话的哈希正确无误, 从而保证了在Flask中使用数据时数据的完整性。就我们而言, 我们不太担心客户端会篡改会话, 因为:
  • 我们没有使用基于Cookie的会话, 也就是说, 我们没有将所有用户数据发送到客户端。
  • 在Flask上, 我们需要一个只读的SessionStore, 它将告诉我们给定的密钥是否存在, 并返回存储的数据。
扩展到Flask 接下来, 让我们创建Redis会话引擎(数据库)的简化版本以与Flask一起使用。我们将使用与基类相同的SessionStore(上面定义), 但是我们需要删除其某些功能, 例如, 检查错误的签名或修改会话。我们对只读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 JSONSerializerdef load(self): session_data = http://www.srcmini.com/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 = http://www.srcmini.com/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 (i.e., empty session). return {}

我们只需要load方法, 因为它是存储的只读实现。这意味着你无法直接从Flask中注销;相反, 你可能需要将此任务重定向到Django。请记住, 这里的目标是管理这两个Python框架之间的会话, 从而为你提供更大的灵活性。
Flask会话 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_fordef 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_applicationapplication = get_wsgi_application()d = wsgiserver.WSGIPathInfoDispatcher({ "/":application, "/backend":app }) server = wsgiserver.CherryPyWSGIServer(("127.0.0.1", 8080), d)

这样, Django将在” /” 上运行, 而Flask将在” /后端” 端点上运行。
结论 【Django,Flask和Redis教程(Python框架之间的Web应用程序会话管理)】我将Django和Flask焊接在一起, 而不是研究Django与Flask或鼓励你只学习Flask微框架, 而是通过将任务委托给Django来使它们共享相同的会话数据以进行身份??验证。由于Django附带了许多模块来解决用户注册, 登录和注销(仅举几个例子), 将这两个框架结合起来可以节省你宝贵的时间, 同时为你提供了破解Flask等可管理的微框架的机会。

    推荐阅读