# 内置session理解
# session 介绍
在解析 session 的实现之前,我们先介绍一下 session 怎么使用。session 可以看做是在不同的请求之间保存数据的方法,因为 HTTP 是无状态的协议,但是在业务应用上我们希望知道不同请求是否是同一个人发起的。比如张三,王一都在自己的手机上用淘宝购物,将想购买的商品放入购物车中,当王一,张三结账时,不能将他俩的购物车混淆了,服务器区分和保存购物车数据的方法就是session。
# flask的session是基于cookie的会话保持。简单的原理即:
当客户端进行第一次请求时,客户端的HTTP request(cookie为空)到服务端,服务端创建session,视图函数根据form表单填写session,请求结束时,session内容填写入response的cookie中并返回给客户端,客户端的cookie中便保存了用户的数据。当同一客户端再次请求时, 客户端的HTTP request中cookie已经携带数据,视图函数根据cookie中值做相应操作(如已经携带用户名和密码就可以直接登陆)
# 简单使用Flask的内置session
在 flask 中使用 session 也很简单,只要使用 from flask import session 导入这个变量,在代码中就能直接通过读写它和 session 交互。
from flask import Flask, session, escape, request app = Flask(__name__) app.secret_key = 'please-generate-a-random-secret_key' #内置session必须导入secret_key @app.route("/") def index(): if 'username' in session: return 'hello, {}\n'.format(escape(session['username'])) return 'hello, stranger\n' @app.route("/login", methods=['POST']) def login(): session['username'] = request.form['username'] return 'login success' if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)
上面这段代码模拟了一个非常简单的登陆逻辑,用户访问 POST /login 来登陆,后面访问页面的时候 GET /,会返回该用户的名字。
# 请求第一次来时,session是什么时候生成的?存放在哪里?
flask中session使用非常简单,但是实现原理却没那么简单,下面我们通过几个问题来弄清楚session是如何实现的。
在上下文和应用上下文中已经知道session是一个LocalProxy()对象:
current_app = LocalProxy(_find_app) request = LocalProxy(partial(_lookup_req_object, 'request')) session = LocalProxy(partial(_lookup_req_object, 'session')) g = LocalProxy(partial(_lookup_app_object, 'g'))
客户端的请求进来时,会调用app.wsgi_app():
def wsgi_app(self, environ, start_response): ctx = self.request_context(environ) error = None try: try: ctx.push() # 寻找视图函数,并执行 # 获取返回值 response response = self.full_dispatch_request()
此时执行第一句话,会生成一个ctx,其本质是一个RequestContext对象:
class RequestContext(object): def __init__(self, app, environ, request=None): self.app = app if request is None: request = app.request_class(environ) self.request = request self.url_adapter = app.create_url_adapter(self.request) self.flashes = None self.session = None
在RequestContext 对象中定义了session,且初值为None。
接着继续看wsgi_app函数中,ctx.push()函数:
def push(self): app_ctx = _app_ctx_stack.top if app_ctx is None or app_ctx.app != self.app: app_ctx = self.app.app_context() app_ctx.push() self._implicit_app_ctx_stack.append(app_ctx) else: self._implicit_app_ctx_stack.append(None) if hasattr(sys, 'exc_clear'): sys.exc_clear() _request_ctx_stack.push(self) #添加ctx进请求上下文中 if self.session is None: #第一次进来肯定为None session_interface = self.app.session_interface self.session = session_interface.open_session( self.app, self.request ) if self.session is None: self.session = session_interface.make_null_session(self.app)
前半部分代码已经在之前的文章中讲到,主要看后半部分代码。判断session是否为空,我在RequestContext 中看到session初值为空.
在 Flask 中,所有和 session 有关的调用,都是转发到 self.session_interface 的方法调用上(这样用户就能用自定义的 session_interface 来控制 session 的使用)。而默认的 session_inerface 有默认值:
session_interface = SecureCookieSessionInterface()
是一个继承SessionInterface的类,里面各种涉及session的方法和属性,包括session的加密和序列化、反序列化执行SecureCookieSessionInterface.open_session()来生成默认session对象:注意open_session有多个版本,分别看你使用的是那个session,有内置的,也有如:redis数据库的等
def open_session(self, app, request): 获取session签名的算法 s = self.get_signing_serializer(app) 如果为空 直接返回None if s is None: return None val = request.cookies.get(app.session_cookie_name) # 如果val为空,即request.cookies为空 if not val: return self.session_class() max_age = total_seconds(app.permanent_session_lifetime) try: data = s.loads(val, max_age=max_age) return self.session_class(data) except BadSignature: return self.session_class()
请求第一次来时,request.cookies为空,即返回self.session_class():
class SecureCookieSessionInterface(SessionInterface): ... session_class = SecureCookieSession
看SecureCookieSession:
class SecureCookieSession(CallbackDict, SessionMixin): modified = False accessed = False def __init__(self, initial=None): def on_update(self): self.modified = True self.accessed = True super(SecureCookieSession, self).__init__(initial, on_update) def __getitem__(self, key): self.accessed = True return super(SecureCookieSession, self).__getitem__(key) def get(self, key, default=None): self.accessed = True return super(SecureCookieSession, self).get(key, default) def setdefault(self, key, default=None): self.accessed = True return super(SecureCookieSession, self).setdefault(key, default)
看其继承关系,其实就是一个特殊的字典。到此我们知道了session就是一个特殊的字典,调用SecureCookieSessionInterface类的open_session()创建,并保存在ctx中,即RequestContext对象中。但最终由session = LocalProxy(..., 'session')对象代为管理,到此,在视图函数中就可以导入session并使用了。
# 当请求第二次来时,session生成的是什么?
当请求第二次到来时,与第一次的不同就在open_session()那个val判断处,此时cookies不为空, 获取cookie的有效时长,如果cookie依然有效,通过与写入时同样的签名算法将cookie中的值解密出来并写入字典并返回中,若cookie已经失效,则仍然返回'空字典'。
# 特殊的SecureCookieSession字典有那些功能?如何实现的?
默认的 session 对象是 SecureCookieSession,这个类就是一个基本的字典,外加一些特殊的属性,比如 permanent(flask 插件会用到这个变量)、modified(表明实例是否被更新过,如果更新过就要重新计算并设置 cookie,因为计算过程比较贵,所以如果对象没有被修改,就直接跳过)。
怎么知道实例的数据被更新过呢? SecureCookieSession 是基于 werkzeug/datastructures:CallbackDict 实现的,这个类可以指定一个函数作为 on_update 参数,每次有字典操作的时候(__setitem__、__delitem__、clear、popitem、update、pop、setdefault
)会调用这个函数。
查看SecureCookieSession:
class SecureCookieSession(CallbackDict, SessionMixin): modified = False accessed = False def __init__(self, initial=None): def on_update(self): self.modified = True self.accessed = True #将on_update()传递给CallbackDict super(SecureCookieSession, self).__init__(initial, on_update) def __getitem__(self, key): self.accessed = True return super(SecureCookieSession, self).__getitem__(key) def get(self, key, default=None): self.accessed = True return super(SecureCookieSession, self).get(key, default) def setdefault(self, key, default=None): self.accessed = True return super(SecureCookieSession, self).setdefault(key, default)
继承的 CallbackDict:
class CallbackDict(UpdateDictMixin, dict): def __init__(self, initial=None, on_update=None): dict.__init__(self, initial or ()) self.on_update = on_update def __repr__(self): return '<%s %s>' % ( self.__class__.__name__, dict.__repr__(self) )
CallbackDict又继承UpdateDictMixin:
class UpdateDictMixin(object): on_update = None def oncall(self, *args, **kw): rv = getattr(super(UpdateDictMixin, self), name)(*args, **kw) if self.on_update is not None: self.on_update(self) return rv oncall.__name__ = name return oncall def setdefault(self, key, default=None): modified = key not in self rv = super(UpdateDictMixin, self).setdefault(key, default) if modified and self.on_update is not None: self.on_update(self) return rv def pop(self, key, default=_missing): modified = key in self if default is _missing: rv = super(UpdateDictMixin, self).pop(key) else: rv = super(UpdateDictMixin, self).pop(key, default) if modified and self.on_update is not None: self.on_update(self) return rv __setitem__ = calls_update('__setitem__') __delitem__ = calls_update('__delitem__') clear = calls_update('clear') popitem = calls_update('popitem') update = calls_update('update') del calls_update
由UpdateDictMixin()可知,对session进行改动会调用
pop, __setitem__
等方法,同时就会调用on_update()方法,从而修改modify,security的值。
# 签名算法
获取 cookie 数据的过程中,最核心的几句话是:
s = self.get_signing_serializer(app)
val = request.cookies.get(app.session_cookie_name)
data = s.loads(val, max_age=max_age)
return self.session_class(data)
其中两句都和 s 有关,signing_serializer 保证了 cookie 和 session 的转换过程中的安全问题。如果 flask 发现请求的 cookie 被篡改了,它会直接放弃使用。
我们继续看 get_signing_serializer 方法:
def get_signing_serializer(self, app):
if not app.secret_key:
return None
signer_kwargs = dict(
key_derivation=self.key_derivation,
digest_method=self.digest_method
)
return URLSafeTimedSerializer(app.secret_key,
salt=self.salt,
serializer=self.serializer,
signer_kwargs=signer_kwargs)
我们看到这里需要用到很多参数:
secret_key:密钥。这个是必须的,如果没有配置 secret_key 就直接使用 session 会报错
salt:为了增强安全性而设置一个 salt 字符串(可以自行搜索“安全加盐”了解对应的原理)
serializer:序列算法
signer_kwargs:其他参数,包括摘要/hash算法(默认是 sha1)和 签名算法(默认是 hmac)
URLSafeTimedSerializer: 是 itsdangerous 库的类,主要用来进行数据验证,增加网络中数据的安全性。itsdangerours提供了多种 Serializer,可以方便地进行类似 json 处理的数据序列化和反序列的操作。至于具体的实现,因为篇幅限制,就不解释了。
# session什么时候写入cookie中?session的生命周期?
前面的几个问题实际上都发生在wsgi_app()前两句函数中,主要就是ctx.push()函数中,下面看看wsgi_app()后面干了嘛:
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
error = None
try:
try:
# ctx.push函数是前半部分最重要的一个函数
# 生成request和session并将二者保存到RequestContext()对象ctxz中
# 最后将ctx,push到LocalStack()对象_request_ctx_stack中
ctx.push()
# 寻找视图函数,并执行
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except:
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
# 最后, 将自己请求在local中的数据清除
ctx.auto_pop(error)
看full_dispatch_request:
def full_dispatch_request(self): #执行before_first_request self.try_trigger_before_first_request_functions() try: # 触发request_started 信号 request_started.send(self) # 调用before_request rv = self.preprocess_request() if rv is None: #执行视图函数 rv = self.dispatch_request() except Exception as e: rv = self.handle_user_exception(e) return self.finalize_request(rv)
前半部分就在执行flask钩子,before_first_request, before_request以及信号,接着执行视图函数生成rv,我们主要看finalize_request(rv):
def finalize_request(self, rv, from_error_handler=False): response = self.make_response(rv) try: response = self.process_response(response) request_finished.send(self, response=response) except Exception: if not from_error_handler: raise self.logger.exception('Request finalizing failed with an ' 'error while handling an error') return response
首先根据rv生成response。再执行process_response:
def process_response(self, response): ctx = _request_ctx_stack.top bp = ctx.request.blueprint funcs = ctx._after_request_functions if bp is not None and bp in self.after_request_funcs: funcs = chain(funcs, reversed(self.after_request_funcs[bp])) if None in self.after_request_funcs: funcs = chain(funcs, reversed(self.after_request_funcs[None])) for handler in funcs: response = handler(response) if not self.session_interface.is_null_session(ctx.session): self.session_interface.save_session(self, ctx.session, response) return response
前半部分主要执行flask的钩子,看后面,判断,session是否为空,如果不为空,则执行save_session():
def save_session(self, app, session, response): domain = self.get_cookie_domain(app) path = self.get_cookie_path(app) # If the session is modified to be empty, remove the cookie. # If the session is empty, return without setting the cookie. if not session: if session.modified: response.delete_cookie( app.session_cookie_name, domain=domain, path=path ) return # Add a "Vary: Cookie" header if the session was accessed at all. if session.accessed: response.vary.add('Cookie') if not self.should_set_cookie(app, session): return httponly = self.get_cookie_httponly(app) secure = self.get_cookie_secure(app) samesite = self.get_cookie_samesite(app) expires = self.get_expiration_time(app, session) val = self.get_signing_serializer(app).dumps(dict(session)) response.set_cookie( app.session_cookie_name, val, expires=expires, httponly=httponly, domain=domain, path=path, secure=secure, samesite=samesite )
save_session()比较简单,且有注释,便不再讲解,主要就是将session写入response.set_cookie中。这样便完成session的写入response工作,并由response返回至客户端。
再请求结束时会执行wsgi_app()的finally:ctx.auto_pop(error)函数,将与对应请求相关的request,session清除,session生命周期便结束。
# 总结:
其主要的就是SecureCookieSessionInterface对象的open_session()与save_session() 。open_session在请求刚进来时执行,完成session对象的创建(就是一特殊字典),在视图函数中完成对session的赋值操作,save_session()在视图函数执行完后,生成response后执行,将session写入response的cookie中。
当然,flask内置session无法满足生产需求。因为将session数据全部保存在cookie中不安全且cookie存储数据量有限,但flask-session组件帮我们实现了将数据保存在服务器''数据库''中而只将sessionID保存在cookie中,下一节便会讲解flask-session组建的原理。