作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Denis Kyorov的头像

Denis Kyorov

Denis是一位经验丰富的Python/Go开发人员(7年以上),他为各种具有挑战性的后端项目做出了贡献.

Expertise

Previously At

Datapipe
Share

Django vs Flask: Django是错误的选择

I love and use Django 在我的很多个人和客户项目中, 主要用于更经典的web应用程序和涉及关系数据库的应用程序. 然而,Django并不是灵丹妙药.

在设计上,Django与它的ORM、模板引擎系统和设置对象是紧密耦合的. 另外,它不是一个新项目:为了保持向后兼容,它背负了很多包袱.

一些Python开发人员认为这是一个主要问题. 他们说Django不够灵活,应该尽可能避免使用它, instead, 使用Python微框架,比如Flask.

I don’t share that opinion. Django在 appropriate place and time,即使它不适合 every project spec. 俗话说:“为工作使用正确的工具”。.

(即使在不合适的时间和地点, 有时候用Django编程会有独特的好处.)

在某些情况下,使用更轻量级的框架(如 Flask). 通常,当您意识到这些微框架是多么容易破解时,它们就会开始发光发热.

拯救微框架

在我的一些客户项目中, 我们已经讨论过放弃Django而转向微框架, 通常当客户想要做一些有趣的事情时(在一个案例中), for example, embedding ZeroMQ 在应用程序对象中)和项目目标似乎更难以用Django实现.

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

  • Simple REST API backends
  • 不需要数据库访问的应用程序
  • NoSQL-based web apps
  • 具有非常特定需求的Web应用程序,如自定义URL配置

At the same time, 我们的应用程序需要用户注册和其他一些Django在几年前就解决了的常见任务. 考虑到它的轻量级,Flask没有提供相同的工具包.

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

问题出现了:Django是一个孤注一掷的交易吗? Should we drop it completely from the project, 或者我们可以学习将它与其他微框架或传统框架的灵活性结合起来? 我们可以挑选我们想要使用的部分而避开其他部分吗?

我们能两全其美吗? 我的回答是肯定的,尤其是在会话管理方面.

(更不用说,还有很多项目是为Django自由职业者准备的.)

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

这篇文章的目标是将用户认证和注册的任务委托给Django, 还可以使用Redis与其他框架共享用户会话. 我可以想到一些场景,其中像这样的东西将是有用的:

  • 你需要在Django应用程序之外单独开发一个REST API,但又希望共享会话数据.
  • 您有一个特定的组件,由于某种原因可能需要稍后更换或扩展,并且仍然需要会话数据.

对于本教程,我将使用 Redis 在两个框架之间共享会话(在本例中是Django和Flask). 在当前的设置中,我将使用 SQLite to store user information, 但如果需要,您可以将后端绑定到NoSQL数据库(或基于sql的替代方案).

Understanding Sessions

在Django和Flask之间共享会话, 我们需要了解Django是如何存储会话信息的. The Django docs 都很好,但为了完整起见,我将提供一些背景知识.

会话管理品种

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

  • Cookie-based sessions:该场景下,会话数据不存储在后端数据存储中. 相反,它被序列化、签名(用一个SECRET_KEY),然后发送到客户端. 当客户端发回数据时, 它的完整性被检查是否被篡改,并在服务器上再次反序列化.

  • Storage-based sessions:在此场景中,会话数据本身是 not sent to the client. Instead, 只发送一小部分(密钥)来指示当前用户的身份, 存储在会话存储中.

In our example, 我们对后一种场景更感兴趣:我们希望会话数据存储在后端,然后在Flask中进行检查. 前者也可以做同样的事情,但正如Django文档所提到的,有一些 对安全的担忧 of the first method.

The General Workflow

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

使用Redis管理Flask和Django之间的用户会话.

让我们更详细地介绍会话共享:

  1. 当一个新请求进来时,第一步是通过已注册的 middleware in the Django stack. 我们感兴趣的是 SessionMiddleware 类,如你所料,它与会话管理和处理相关:

    类SessionMiddleware(对象):
    
        Def process_request(self, request):
            引擎= import_module(设置.SESSION_ENGINE)
            session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
            request.session = engine.SessionStore(session_key)
    

    在这个代码片段中,Django获取注册的 SessionEngine (我们很快就会讲到),提取 SESSION_COOKIE_NAME from request (sessionid(默认情况下),并创建所选对象的新实例 SessionEngine to handle session storage.

  • 稍后(在处理用户视图之后), 但仍然在中间件堆栈中), 会话引擎调用其save方法将任何更改保存到数据存储中. (在视图处理期间,用户可能在会话中更改了一些东西,例如.g.,通过向会话对象添加一个新的值 request.session.) Then, the SESSION_COOKIE_NAME is sent to the client. 下面是简化版:

    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,
                    域= =到期,到期设置.SESSION_COOKIE_DOMAIN,
                    path=settings.SESSION_COOKIE_PATH,
                    secure=settings.SESSION_COOKIE_SECURE或None,
                    httponly=settings.(SESSION_COOKIE_HTTPONLY或None)
    
        return response
    

我们特别感兴趣的是 SessionEngine 类,我们将用一些东西来存储和加载数据到Redis后端.

幸运的是,有一些项目已经为我们处理了这个问题. Here’s an example from redis_sessions_fork. Pay close attention to the save and load 方法,这是为了(分别)存储和加载会话到和从Redis:

类SessionStore (SessionBase):
    """
    Django的Redis会话后端
    """
    def __init__(self, session_key=None):
        super(SessionStore, self).__init__(session_key)

    def _get_or_create_session_key(自我):
        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)
        如果不是,session_data为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.保存(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_module

>>> 引擎= import_module(设置.SESSION_ENGINE)
>>> engine.SessionStore()


>>> store["count"] = 1
>>> store.save()
>>> store.load()
{u'count': 1}

会话存储的接口非常容易理解, 但这背后有很多秘密. 我们应该深入挖掘一下,以便在Flask上实现类似的东西.

注意:您可能会问,“为什么不直接将SessionEngine复制到Flask中呢??” Easier said than done. 正如我们一开始讨论的, 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-encoded string. 为了理解它的目的,我们需要看看Django的 SessionBase 类来查看它是如何处理的:

class SessionBase(object):
    """
    所有Session类的基类.
    """

    Def encode(self, session_dict):
        返回给定的会话字典序列化并编码为字符串."
        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:
            哈希,serialized = encoded_data.split(b':', 1)
            expected_hash = self._hash(serialized)
            如果不是,constant_time_compare(hash.decode(), expected_hash):
                抛出可疑会话("Session data corrupted")
            else:
                return self.serializer().loads(serialized)
        except Exception as e:
            # ValueError, SuspiciousOperation, unpickling异常
            if isinstance(e, SuspiciousOperation):
                logger = logging.getLogger('django.security.%s' %
                        e.__class__.__name__)
                logger.warning(force_text(e))
            return {}

encode方法首先用当前注册的序列化程序序列化数据. In other words, 它将会话转换为字符串, 稍后它可以将其转换回会话(查看SESSION_SERIALIZER文档了解更多信息)。. Then, 它对序列化的数据进行散列,并在稍后使用该散列作为签名来检查会话数据的完整性. 最后,它将该数据对作为base64编码的字符串返回给用户.

顺便说一下:在版本1之前.Django默认使用pickle来序列化会话数据. Due to security concerns,默认的序列化方法为now django.contrib.sessions.serializers.JSONSerializer.

Encoding an Example Session

让我们看看会话管理过程的实际情况. Here, 我们的会话字典只是一个计数和一个整数, 但你可以想象这将如何推广到更复杂的用户会话.

>>> store.encode({'count': 1})
u 'ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ = = '

>>> base64.b64decode(encoded)
“fe1964e1d2cf8069d9f1823afd143400b6d3736f:{“计数”:1}’

存储方法(u ' zmuxoty…== ')的结果是一个包含序列化的用户会话的编码字符串 and its hash. 当我们解码它时,我们确实得到了哈希值(' fe1964e…')和会话({"count":1}).

请注意,decode方法进行检查,以确保该会话的哈希值是正确的, 保证数据在Flask中使用时的完整性. 在我们的例子中,我们并不太担心我们的会话在客户端被篡改,因为:

  • 我们没有使用基于cookie的会话.e., we’re not sending all user data to the client.

  • 在Flask上,我们需要一个只读的 SessionStore 它会告诉我们给定的键是否存在并返回存储的数据.

Extending to Flask

接下来,让我们创建一个简化版本的Redis会话引擎(数据库)来与Flask一起工作. We’ll use the same SessionStore (如上定义)作为基类,但是我们需要删除它的一些功能,例如.g.,检查错误签名或修改会话. 我们对只读更感兴趣 SessionStore 它将加载从Django中保存的会话数据. 让我们看看它们是如何组合在一起的:

类SessionStore(对象):

    #默认序列化器,目前
    def __init__(self, conn, session_key, secret, serializer=None):

        self._conn = conn
        self.session_key = session_key
        self._secret = secret
        self.serializer = serializer或JSONSerializer

    def load(self):
        session_data = self._conn.get(self.session_key)

        如果不是,session_data为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:
            #如果没有':',可能会产生ValueError
            哈希,serialized = encoded_data.split(b':', 1)
            在Django版本中,他们会检查损坏的数据
            #我觉得它没有用,所以我要删除它
            return self.serializer().loads(serialized)
        except Exception as e:
            # ValueError, SuspiciousOperation, unpickling异常. If any of
            #这些发生,返回一个空字典(i.e., empty session).
            return {}

We only need the load 方法,因为它是存储的只读实现. That means you can’t logout directly from Flask; instead, 你可能想把这个任务重定向到Django. Remember, 这里的目标是管理这两个Python框架之间的会话,为您提供更大的灵活性.

Flask Sessions

Flask微框架支持基于cookie的会话, 这意味着所有的会话数据都被发送到客户端, base64编码和加密签名. 但实际上,我们对Flask的会话支持不太感兴趣.

我们需要的是获得Django创建的会话ID,并在Redis后端进行检查,这样我们就可以确定请求属于预签名的用户. 总之,理想的过程是(这与上面的图表同步):

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

如果有一个装饰器来检查这些信息并设置电流,将会很方便 user_id into the g variable in Flask:

从functools导入包装
从flask导入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数据. If the session has an _auth_user_id, we return the content from the view function; otherwise, 用户被重定向到登录页面, just like we wanted.

Gluing Things Together

为了共享cookie,我发现通过a来启动Django和Flask是很方便的 WSGI 服务器并将它们粘合在一起. In this example, I’ve used CherryPy:

from app import app
from django.core.导入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”端点.

In Conclusion

而不是对比Django和Flask,或者鼓励你只学习Flask微框架, 我把姜戈和弗拉斯克焊接在一起了, 通过将任务委托给Django,让它们共享相同的会话数据进行身份验证. 因为Django附带了很多模块来解决用户注册问题, login, 登出(仅举几个例子), 将这两个框架结合起来将节省您宝贵的时间,同时为您提供了开发可管理的微框架(如Flask)的机会.

就这一主题咨询作者或专家.
Schedule a call
Denis Kyorov的头像
Denis Kyorov

Located in London, United Kingdom

Member since February 4, 2014

About the author

Denis是一位经验丰富的Python/Go开发人员(7年以上),他为各种具有挑战性的后端项目做出了贡献.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Previously At

Datapipe

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.