个性化阅读
专注于IT技术分析

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

点击下载

本文概述

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文档所提到的, 第一种方法的安全性令人担忧。

一般工作流程

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

该图显示了使用Redis管理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(我们将很快处理), 从请求(默认为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_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}'

存储方法(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 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 (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_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将在” /后端”端点上运行。

结论

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

赞(0)
未经允许不得转载:srcmini » Django,Flask和Redis教程:Python框架之间的Web应用程序会话管理

评论 抢沙发

评论前必须登录!