概述
Session 在一个 Web 应用中经常用到,虽然随着近年来 REST API 的不断兴起,session
被用得较少了,但是,还是有非常多的地方用到,即使是 RESTAPI,后台管理用的系统常常也是离不开 session 作为登陆认证使用。本文将讲解 Flask 中 session 的使用,以及底层实现,还将讲述如何自定义 session 管理接口。
Flask Session 使用
在 Flask 中,session 的操作如同操作 cookie 一般简单,因为 session 的实现机理,所以不用担心不同用户不同客户端 session
混淆的问题,也不用但心同一用户多客户统一的问题。
下面,就先以一个简单的控制用户登陆的示例,讲解一下如何使用 session:
from flask import Flask, session, redirect, url_for, escape, request
app = Flask(__name__)
@app.route('/')
def index():
if 'username' in session:
return 'Logged in as %s' % escape(session['username'])
return 'You are not logged in'
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
session['username'] = request.form['username']
return redirect(url_for('index'))
return '''
<form action="" method="post">
<p><input type=text name=username>
<p><input type=submit value=Login>
</form>
'''
@app.route('/logout')
def logout():
# remove the username from the session if it's there
session.pop('username', None)
return redirect(url_for('index'))
# set the secret key. keep this really secret:
app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT'
这是一个官方的例子,这个例子中有几个地方值得探讨,分别是:
- app.securet_key
- from flask import session
- return ‘Logged in as %s’ % escape(session[‘username’])
- session[‘username’] = request.form[‘username’]
- session.pop(‘username’, None)
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:
>>> import os
>>> os.urandom(24)
'\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 最起码需要实现两个方法,分别是:
- open_session:通过请求的参数创建对应的 session 对象
- save_session:将 session 对象保存到本地以及 response 中
所以,如果我们要将 session 保存在 redis,我们可以这样做:
- 想创建一个 RedisSessionInterface
- 替换 flask 的 session_interface 即可。
创建 RedisSessionInterface
import pickle
from datetime import timedelta
from uuid import uuid4
from redis import Redis
from werkzeug.datastructures import CallbackDict
from flask.sessions import SessionInterface, SessionMixin
class RedisSession(CallbackDict, SessionMixin):
def __init__(self, initial=None, sid=None, new=False):
def on_update(self):
self.modified = True
CallbackDict.__init__(self, initial, on_update)
self.sid = sid
self.new = new
self.modified = False
class RedisSessionInterface(SessionInterface):
serializer = pickle
session_class = RedisSession
def __init__(self, redis=None, prefix='session:'):
if redis is None:
redis = Redis()
self.redis = redis
self.prefix = prefix
def generate_sid(self):
return str(uuid4())
def get_redis_expiration_time(self, app, session):
if session.permanent:
return app.permanent_session_lifetime
return timedelta(days=1)
def open_session(self, app, request):
sid = request.cookies.get(app.session_cookie_name)
if not sid:
sid = self.generate_sid()
return self.session_class(sid=sid, new=True)
val = self.redis.get(self.prefix + sid)
if val is not None:
data = self.serializer.loads(val)
return self.session_class(data, sid=sid)
return self.session_class(sid=sid, new=True)
def save_session(self, app, session, response):
domain = self.get_cookie_domain(app)
if not session:
self.redis.delete(self.prefix + session.sid)
if session.modified:
response.delete_cookie(app.session_cookie_name,
domain=domain)
return
redis_exp = self.get_redis_expiration_time(app, session)
cookie_exp = self.get_expiration_time(app, session)
val = self.serializer.dumps(dict(session))
self.redis.setex(self.prefix + session.sid, val,
int(redis_exp.total_seconds()))
response.set_cookie(app.session_cookie_name, session.sid,
expires=cookie_exp, httponly=True,
domain=domain)
这里主要就是重写了 open_session 和 save_session 两个方法,其他方法和类都是辅助用的,当然, 既然 Session
的存放位置改变了,那么 Session Class 也需要重新定义一下,重要的一点:别忘了 Session Class 要继承
SessionMixin。
接着替换掉 flask 的 session_interface 即可:
app = Flask(__name__)
app.session_interface = RedisSessionInterface()
就这样,当我们和平时一样操作 session 的时候,其实内部用的就是我们自定义的 session_interface 了。
Session 操作
替换 session 其实也就这么简单,接下来看看 Flask 是如何利用 session_interface 来操作 session 的。
首先看到 session 的定义,位置在:
flask/globals.py
line 0060: session = LocalProxy(partial(_lookup_req_object, 'session'))
可以看到 session 其实也是一个代理,其实是 partial 做了一层封装,那么转化一下就是:
session['name'] = _lookup_req_object('session')['name']
_lookup_req_object
这个函数我们已经见过好多次了,其实此时就是需要关注的还是老问题,就是 request
- 何时入栈
- 何时出栈
何时入栈
我们还是从代码的入口开始找起,flask 的路口一般是 wsgi_app,所以我们还是想找到:
flask/app.py
... ...
line 1916: def request_context(self, environ):
... ...
line 1944: return RequestContext(self, environ)
... ...
line 1958: def wsgi_app(self, environ, start_response):
... ...
line 1983: ctx = self.request_context(environ)
首先是看 wsgi_app,这里有一个创建请求上下文的地方,然后调用的是 line: 1916 中的 request_context 方法,而
request_context 方法里面是直接创建了一个 RequestContext 变量,那我们就看下这个变量的创建过程:
flask/ctx.py
line 0207: class RequestContext(object):
... ...
line 0237: def __init__(self, app, environ, request=None):
... ...
line 0244: self.session = None
... ...
line 0297: def push(self):
... ...
line 0332: self.session = self.app.open_session(self.request)
line 0333: if self.session is None:
line 0334: self.session = self.app.make_null_session()
可以发现 session 在这里就被创建了,并且自然得放进了 请求上下文 中,这样的话,我们也就是可以通过访问 flask.session
来直接调用了。但是,我们可以发现 session 对象创建的方法却不是我们预想中的使用 session_interface,而是调用了 app
的方法,那我们就看一下 app 中的这些方法是怎么实现的:
line 0906: def open_session(self, request):
... ...
line 0914: return self.session_interface.open_session(self, request)
... ...
line 0916: def save_session(self, session, response):
... ...
line 0926: return self.session_interface.save_session(self, session, response)
... ...
line 0928: def make_null_session(self):
... ...
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
line 0290: class SecureCookieSessionInterface(SessionInterface):
line 0319: def open_session(self, app, request):
line 0320: s = self.get_signing_serializer(app)
line 0321: if s is None:
line 0322: return None
line 0323: val = request.cookies.get(app.session_cookie_name)
line 0324: if not val:
line 0325: return self.session_class()
line 0326: max_age = total_seconds(app.permanent_session_lifetime)
line 0327: try:
line 0328: data = s.loads(val, max_age=max_age)
line 0329: return self.session_class(data)
line 0330: except BadSignature:
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 的内容:
line 0333: def save_session(self, app, session, response):
line 0334: domain = self.get_cookie_domain(app)
line 0335: path = self.get_cookie_path(app)
... ...
line 0340: if not session:
line 0341: if session.modified:
line 0342: response.delete_cookie(app.session_cookie_name,
line 0343: domain=domain, path=path)
line 0344: return
... ...
line 0353: if not self.should_set_cookie(app, session):
line 0354: return
line 0355:
line 0356: httponly = self.get_cookie_httponly(app)
line 0357: secure = self.get_cookie_secure(app)
line 0358: expires = self.get_expiration_time(app, session)
line 0359: val = self.get_signing_serializer(app).dumps(dict(session))
line 0360: response.set_cookie(app.session_cookie_name, val,
line 0361: expires=expires, httponly=httponly,
line 0362: domain=domain, path=path, secure=secure)
save_session 的工作就更简单了,前面都做了一个变量的获取操作,关键的在于 line 359,这里对session 的内容进行一个加密,然后在
line 360 将加密后的 session 保存到cookie 中,返回给客户端,等待客户端下次请求传递回来。
扩展思路
如何实现多子域名下多应用的 session 同步
对于一个企业级应用,我们可能会有好多个系统,每个系统都有一个独立的子域名,例如一个电子商务系统,可能我们的商品系统、购物车、订单系统都是不同的子域名,假设是这样:
商品系统:item.mdpress.com
购物车: cart.mdpress.com
订单系统:order.mdpress.com
那我们为了给用户一个良好的体验,肯定是希望用户如果在一个子系统中登陆了,在其他子系统也能共享登陆状态的。那么基于这个需求,该如何实现呢?
这个问题有一点点复杂,但是,总的思路来说使用默认的 session 应该是实现不了的,但是,如果使用我们第二段中提到的
RedisSessionInterface 是很容易实现的,这个就不细说了,如果有兴趣,可以留言参与讨论。