作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Denis是一位经验丰富的Python/Go开发人员(7年以上),他为各种具有挑战性的后端项目做出了贡献.
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用于:
At the same time, 我们的应用程序需要用户注册和其他一些Django在几年前就解决了的常见任务. 考虑到它的轻量级,Flask没有提供相同的工具包.
问题出现了:Django是一个孤注一掷的交易吗? Should we drop it completely from the project, 或者我们可以学习将它与其他微框架或传统框架的灵活性结合起来? 我们可以挑选我们想要使用的部分而避开其他部分吗?
我们能两全其美吗? 我的回答是肯定的,尤其是在会话管理方面.
(更不用说,还有很多项目是为Django自由职业者准备的.)
这篇文章的目标是将用户认证和注册的任务委托给Django, 还可以使用Redis与其他框架共享用户会话. 我可以想到一些场景,其中像这样的东西将是有用的:
对于本教程,我将使用 Redis 在两个框架之间共享会话(在本例中是Django和Flask). 在当前的设置中,我将使用 SQLite to store user information, 但如果需要,您可以将后端绑定到NoSQL数据库(或基于sql的替代方案).
在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.
会话处理和管理的一般工作流程类似于下图:
让我们更详细地介绍会话共享:
当一个新请求进来时,第一步是通过已注册的 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做了很多工作来掩盖会话存储的复杂性. 让我们检查一下存储在上面代码片段中的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
.
让我们看看会话管理过程的实际情况. 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
它会告诉我们给定的键是否存在并返回存储的数据.
接下来,让我们创建一个简化版本的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微框架支持基于cookie的会话, 这意味着所有的会话数据都被发送到客户端, base64编码和加密签名. 但实际上,我们对Flask的会话支持不太感兴趣.
我们需要的是获得Django创建的会话ID,并在Redis后端进行检查,这样我们就可以确定请求属于预签名的用户. 总之,理想的过程是(这与上面的图表同步):
如果有一个装饰器来检查这些信息并设置电流,将会很方便 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.
为了共享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”端点.
而不是对比Django和Flask,或者鼓励你只学习Flask微框架, 我把姜戈和弗拉斯克焊接在一起了, 通过将任务委托给Django,让它们共享相同的会话数据进行身份验证. 因为Django附带了很多模块来解决用户注册问题, login, 登出(仅举几个例子), 将这两个框架结合起来将节省您宝贵的时间,同时为您提供了开发可管理的微框架(如Flask)的机会.
Located in London, United Kingdom
Member since February 4, 2014
Denis是一位经验丰富的Python/Go开发人员(7年以上),他为各种具有挑战性的后端项目做出了贡献.
世界级的文章,每周发一次.
世界级的文章,每周发一次.
Join the Toptal® community.