本文概述
- Django与Flask:当Django是错误的选择时
- 抢救微框架
- 现在, Python教程:共享Django会话
- 了解会话
- Django会话(反序列化)
- 扩展到Flask
- Flask会话
- 粘合在一起
- 结论
根据设计, Django与它的ORM, 模板引擎系统和设置对象紧密结合。另外, 这不是一个新项目:它需要携带很多行李以保持向后兼容。
一些Python开发人员将此视为主要问题。他们说Django不够灵活, 应尽可能避免使用, 而是使用Flask之类的Python微框架。
我不同意这种观点。即使不是在每个项目规范中都适用, 在适当的时间和地点使用Django还是很棒的。口号是:” 使用正确的工具完成工作” 。
(即使在不正确??的时间和地点, 有时使用Django进行编程也可能具有独特的优势。)
在某些情况下, 使用更轻量级的框架(例如Flask)确实很不错。通常, 当你意识到它们容易被黑客入侵时, 这些微框架就开始发光。
抢救微框架 在我的一些客户项目中, 我们讨论了放弃Django并改用微框架的方法, 通常是当客户想要做一些有趣的事情(例如, 在应用程序对象中嵌入ZeroMQ)时, Django似乎很难实现这些目标。
一般来说, 我发现Flask可用于:
- 简单的REST API后端
- 不需要数据库访问的应用程序
- 基于NoSQL的Web应用
- 具有非常特定要求的Web应用程序, 例如自定义URL配置
问题浮出水面:Django是一项全有还是全无的交易?
问题浮出水面:Django是一项全有还是全无的交易?我们应该将其完全从项目中删除, 还是可以学习将其与其他微框架或传统框架的灵活性相结合?我们可以挑选想要使用的作品并避开他人吗?
我们可以两全其美吗?我说是的, 尤其是在会话管理方面。
(更不用说, Django自由职业者那里有很多项目。)
现在, Python教程:共享Django会话 这篇文章的目的是将用户身份验证和注册任务委托给Django, 同时使用Redis与其他框架共享用户会话。我可以想到一些在这种情况下有用的方案:
- 你需要与Django应用分开开发REST API, 但要共享会话数据。
- 你具有特定的组件, 出于某些原因, 以后可能需要替换或扩展该组件, 但仍需要会话数据。
了解会话 要在Django和Flask之间共享会话, 我们需要了解一些有关Django如何存储其会话信息的知识。 Django文档非常不错, 但我将提供一些背景知识以确保完整性。
会话管理品种
通常, 你可以选择通过以下两种方式之一来管理Python应用的会话数据:
- 基于Cookie的会话:在这种情况下, 会话数据不存储在后端的数据存储中。而是对其进行序列化, 签名(使用SECRET_KEY), 然后发送给客户端。当客户端将该数据发送回时, 将检查其完整性以防篡改, 然后在服务器上再次对它进行反序列化。
- 基于存储的会话:在这种情况下, 会话数据本身不会发送到客户端。取而代之的是, 仅发送一小部分(密钥)以指示存储在会话存储区中的当前用户的身份。
一般工作流程
会话处理和管理的一般工作流程将类似于此图:
文章图片
让我们详细了解会话共享:
收到新请求时, 第一步是通过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, 它将告诉我们给定的密钥是否存在, 并返回存储的数据。
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匹配的会话。
- 如果没有, 我们会将其重定向到登录页面。
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等可管理的微框架的机会。
推荐阅读
- 建立第一个Ember.js应用程序的指南
- AngularJS教程(自定义指令的神秘化)
- 建立WebRTC应用程序的一年(启动工程学)
- Ember Data(Ember数据库的综合教程)
- 你的第一个AngularJS应用的分步教程
- 快速应用程序开发框架AllcountJS进行开发
- 10个最常见的Web安全漏洞
- Web API设计的5条黄金法则
- 为什么我决定拥抱Laravel