概述

Session 在一个 Web 应用中经常用到,虽然随着近年来 REST API 的不断兴起,session
被用得较少了,但是,还是有非常多的地方用到,即使是 RESTAPI,后台管理用的系统常常也是离不开 session 作为登陆认证使用。本文将讲解 Flask 中 session 的使用,以及底层实现,还将讲述如何自定义 session 管理接口。

Flask Session 使用

在 Flask 中,session 的操作如同操作 cookie 一般简单,因为 session 的实现机理,所以不用担心不同用户不同客户端 session
混淆的问题,也不用但心同一用户多客户统一的问题。

下面,就先以一个简单的控制用户登陆的示例,讲解一下如何使用 session:

  1. from flask import Flask, session, redirect, url_for, escape, request
  2. app = Flask(__name__)
  3. @app.route('/')
  4. def index():
  5. if 'username' in session:
  6. return 'Logged in as %s' % escape(session['username'])
  7. return 'You are not logged in'
  8. @app.route('/login', methods=['GET', 'POST'])
  9. def login():
  10. if request.method == 'POST':
  11. session['username'] = request.form['username']
  12. return redirect(url_for('index'))
  13. return '''
  14. <form action="" method="post">
  15. <p><input type=text name=username>
  16. <p><input type=submit value=Login>
  17. </form>
  18. '''
  19. @app.route('/logout')
  20. def logout():
  21. # remove the username from the session if it's there
  22. session.pop('username', None)
  23. return redirect(url_for('index'))
  24. # set the secret key. keep this really secret:
  25. app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT'

这是一个官方的例子,这个例子中有几个地方值得探讨,分别是:

app.securet_key

flask 规定如果想使用默认的 session,那么一定要设置 securet_key,而且最好是不容易猜出来的,官网有给出生成比较安全的
securet_key 的方式。

How to generate good secret keys
The problem with random is that it’s hard to judge what is truly random.
And a secret key should be as random as possible. Your operating system has
ways to generate pretty random stuff based on a cryptographic random generator
which can be used to get such a key:


  1. >>> import os
  2. >>> os.urandom(24)
  3. '\xfd{H\xe5<\x95\xf9\xe3\x96.5\xd1\x01O<!\xd5\xa2\xa0\x9fR"\xa1\xa8'

然后就是复制/黏贴为 securet_key 了。

from flask import session

和其他 request/current_app 一样,直接从 flask 中 import 进来就好了,后面会提到,其实这不是真实的 session
对象,这其实也是一个代理。

session[‘username’]

这是一个典型的使用 session 的例子,使用 session 其实就跟使用 dict 一样,直接使用键就可以访问了,但是,前提是你必须在 session
中设置了值。

session[‘username’] = request.form[‘username’]

这是一个典型的设置 session 值的例子,这里从请求中将 username 提取出来,然后设置给 session,这样,下次就可以直接从 session
中获取到键为 “username” 的值了。

session.pop(‘username’, None)

这个例子是清除 session 的作用,这里将 “username” 从 session 中清除掉,这样以后就不能再使用这个 session 的键了。

Session 的实现

上面就是一些简单使用 session 的例子,接着就讲一下如何替换 session 的实现。

在 Flask 中,默认的创建 session 和保存 session 的操作都是通过一个叫做 session_interface 的接口实现的,这个
session_interface 最起码需要实现两个方法,分别是:

所以,如果我们要将 session 保存在 redis,我们可以这样做:

创建 RedisSessionInterface

  1. import pickle
  2. from datetime import timedelta
  3. from uuid import uuid4
  4. from redis import Redis
  5. from werkzeug.datastructures import CallbackDict
  6. from flask.sessions import SessionInterface, SessionMixin
  7. class RedisSession(CallbackDict, SessionMixin):
  8. def __init__(self, initial=None, sid=None, new=False):
  9. def on_update(self):
  10. self.modified = True
  11. CallbackDict.__init__(self, initial, on_update)
  12. self.sid = sid
  13. self.new = new
  14. self.modified = False
  15. class RedisSessionInterface(SessionInterface):
  16. serializer = pickle
  17. session_class = RedisSession
  18. def __init__(self, redis=None, prefix='session:'):
  19. if redis is None:
  20. redis = Redis()
  21. self.redis = redis
  22. self.prefix = prefix
  23. def generate_sid(self):
  24. return str(uuid4())
  25. def get_redis_expiration_time(self, app, session):
  26. if session.permanent:
  27. return app.permanent_session_lifetime
  28. return timedelta(days=1)
  29. def open_session(self, app, request):
  30. sid = request.cookies.get(app.session_cookie_name)
  31. if not sid:
  32. sid = self.generate_sid()
  33. return self.session_class(sid=sid, new=True)
  34. val = self.redis.get(self.prefix + sid)
  35. if val is not None:
  36. data = self.serializer.loads(val)
  37. return self.session_class(data, sid=sid)
  38. return self.session_class(sid=sid, new=True)
  39. def save_session(self, app, session, response):
  40. domain = self.get_cookie_domain(app)
  41. if not session:
  42. self.redis.delete(self.prefix + session.sid)
  43. if session.modified:
  44. response.delete_cookie(app.session_cookie_name,
  45. domain=domain)
  46. return
  47. redis_exp = self.get_redis_expiration_time(app, session)
  48. cookie_exp = self.get_expiration_time(app, session)
  49. val = self.serializer.dumps(dict(session))
  50. self.redis.setex(self.prefix + session.sid, val,
  51. int(redis_exp.total_seconds()))
  52. response.set_cookie(app.session_cookie_name, session.sid,
  53. expires=cookie_exp, httponly=True,
  54. domain=domain)

这里主要就是重写了 open_session 和 save_session 两个方法,其他方法和类都是辅助用的,当然, 既然 Session
的存放位置改变了,那么 Session Class 也需要重新定义一下,重要的一点:别忘了 Session Class 要继承
SessionMixin。

接着替换掉 flask 的 session_interface 即可:

  1. app = Flask(__name__)
  2. app.session_interface = RedisSessionInterface()

就这样,当我们和平时一样操作 session 的时候,其实内部用的就是我们自定义的 session_interface 了。

Session 操作

替换 session 其实也就这么简单,接下来看看 Flask 是如何利用 session_interface 来操作 session 的。

首先看到 session 的定义,位置在:

flask/globals.py

  1. line 0060: session = LocalProxy(partial(_lookup_req_object, 'session'))

可以看到 session 其实也是一个代理,其实是 partial 做了一层封装,那么转化一下就是:

  1. session['name'] = _lookup_req_object('session')['name']

_lookup_req_object 这个函数我们已经见过好多次了,其实此时就是需要关注的还是老问题,就是 request

  • 何时入栈
  • 何时出栈

何时入栈

我们还是从代码的入口开始找起,flask 的路口一般是 wsgi_app,所以我们还是想找到:

flask/app.py

  1. ... ...
  2. line 1916: def request_context(self, environ):
  3. ... ...
  4. line 1944: return RequestContext(self, environ)
  5. ... ...
  6. line 1958: def wsgi_app(self, environ, start_response):
  7. ... ...
  8. line 1983: ctx = self.request_context(environ)

首先是看 wsgi_app,这里有一个创建请求上下文的地方,然后调用的是 line: 1916 中的 request_context 方法,而
request_context 方法里面是直接创建了一个 RequestContext 变量,那我们就看下这个变量的创建过程:

flask/ctx.py

  1. line 0207: class RequestContext(object):
  2. ... ...
  3. line 0237: def __init__(self, app, environ, request=None):
  4. ... ...
  5. line 0244: self.session = None
  6. ... ...
  7. line 0297: def push(self):
  8. ... ...
  9. line 0332: self.session = self.app.open_session(self.request)
  10. line 0333: if self.session is None:
  11. line 0334: self.session = self.app.make_null_session()

可以发现 session 在这里就被创建了,并且自然得放进了 请求上下文 中,这样的话,我们也就是可以通过访问 flask.session
来直接调用了。但是,我们可以发现 session 对象创建的方法却不是我们预想中的使用 session_interface,而是调用了 app
的方法,那我们就看一下 app 中的这些方法是怎么实现的:

  1. line 0906: def open_session(self, request):
  2. ... ...
  3. line 0914: return self.session_interface.open_session(self, request)
  4. ... ...
  5. line 0916: def save_session(self, session, response):
  6. ... ...
  7. line 0926: return self.session_interface.save_session(self, session, response)
  8. ... ...
  9. line 0928: def make_null_session(self):
  10. ... ...
  11. line 0934: return self.session_interface.make_null_session(self)

可以发现,其实这里就是简单得调用了我们 session_interface 的 open_session 和 save_session 的方法。

何时出栈

出栈的问题还是和 request 一致的,就是看 request 何时出栈的,这个在之前已经解析过了,这里就不累赘了。


整个 Flask 的 Session 的流程就是这样的,那么下面我们就来关注一下 Flask 默认的 Session
的实现是怎样的。正如前面提到的,我们主要关注两个函数: open_session 和 save_session,但是,我们上面看代码的时候还发现了一个
make_null_session,那就顺便看一下 make_null_session ,总共是三个。

flask/sessions.py

  1. line 0290: class SecureCookieSessionInterface(SessionInterface):
  2. line 0319: def open_session(self, app, request):
  3. line 0320: s = self.get_signing_serializer(app)
  4. line 0321: if s is None:
  5. line 0322: return None
  6. line 0323: val = request.cookies.get(app.session_cookie_name)
  7. line 0324: if not val:
  8. line 0325: return self.session_class()
  9. line 0326: max_age = total_seconds(app.permanent_session_lifetime)
  10. line 0327: try:
  11. line 0328: data = s.loads(val, max_age=max_age)
  12. line 0329: return self.session_class(data)
  13. line 0330: except BadSignature:
  14. line 0331: return self.session_class()

首先,这里用到了一个 signing_serializer,这个是用的 itsdanguagers 的一个 URLSafeTimedSerializer
类,这个类可以签名系列化一个对象,并且反系列化回来,而这个系列化依赖于 flask 的 secret_key,这也就是为什么要 session 支持需要设置
securet_key 的原因。

line 323 是从 cookie 中将加密后的 session 对象取出来,然后在 line 328 进行解密,得到数据,然后用这个数据构建
session 对象,然后就放进 request 中使用了。

接着概要得开一下 save_session 的内容:

  1. line 0333: def save_session(self, app, session, response):
  2. line 0334: domain = self.get_cookie_domain(app)
  3. line 0335: path = self.get_cookie_path(app)
  4. ... ...
  5. line 0340: if not session:
  6. line 0341: if session.modified:
  7. line 0342: response.delete_cookie(app.session_cookie_name,
  8. line 0343: domain=domain, path=path)
  9. line 0344: return
  10. ... ...
  11. line 0353: if not self.should_set_cookie(app, session):
  12. line 0354: return
  13. line 0355:
  14. line 0356: httponly = self.get_cookie_httponly(app)
  15. line 0357: secure = self.get_cookie_secure(app)
  16. line 0358: expires = self.get_expiration_time(app, session)
  17. line 0359: val = self.get_signing_serializer(app).dumps(dict(session))
  18. line 0360: response.set_cookie(app.session_cookie_name, val,
  19. line 0361: expires=expires, httponly=httponly,
  20. line 0362: domain=domain, path=path, secure=secure)

save_session 的工作就更简单了,前面都做了一个变量的获取操作,关键的在于 line 359,这里对session 的内容进行一个加密,然后在
line 360 将加密后的 session 保存到cookie 中,返回给客户端,等待客户端下次请求传递回来。


扩展思路

如何实现多子域名下多应用的 session 同步

对于一个企业级应用,我们可能会有好多个系统,每个系统都有一个独立的子域名,例如一个电子商务系统,可能我们的商品系统、购物车、订单系统都是不同的子域名,假设是这样:

  1. 商品系统:item.mdpress.com
  2. 购物车: cart.mdpress.com
  3. 订单系统:order.mdpress.com

那我们为了给用户一个良好的体验,肯定是希望用户如果在一个子系统中登陆了,在其他子系统也能共享登陆状态的。那么基于这个需求,该如何实现呢?

这个问题有一点点复杂,但是,总的思路来说使用默认的 session 应该是实现不了的,但是,如果使用我们第二段中提到的
RedisSessionInterface 是很容易实现的,这个就不细说了,如果有兴趣,可以留言参与讨论。