flask session组件的使用示例
一、简介
flask中session组件可分为内置的session组件还有第三方flask-session组件,内置的session组件功能单一,而第三方的flask-sessoin可支持redis、memcached、文件等进行session的存储。以下将介绍内置session以及第三方session组件的使用方法以及处理机制。
二、内置session处理机制
Cookie与Session
Cookie:
Cookie意为“甜饼”,是由W3C组织提出,最早由Netscape社区发展的一种机制。目前Cookie已经成为标准,所有的主流浏览器如IE、Netscape、Firefox、Opera等都支持Cookie。由于HTTP是一种无状态的协议,服务器单从网络连接上无从知道客户身份。怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。
Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容
Session:
Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了,实质上session就是保存在服务器端的键值对。
如果说Cookie机制是通过检查客户身上的“通行证”来确定客户身份的话,那么Session机制就是通过检查服务器上的“客户明细表”来确认客户身份。Session相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了。
第一次请求,session的创建过程
在flask上下文中介绍了,请求到flask框架会执行wsgi_app方法:
defwsgi_app(self,environ,start_response): ctx=self.request_context(environ)#实例化生成RequestContext对象 error=None try: try: ctx.push()#push上下文到LocalStack中 response=self.full_dispatch_request()#执行视图函数过程 exceptExceptionase: error=e response=self.handle_exception(e)#处理异常 except: error=sys.exc_info()[1] raise returnresponse(environ,start_response) finally: ifself.should_ignore_error(error): error=None ctx.auto_pop(error)#删除LocalStack中的数据
在改方法中会生成一个ctx也就是RequestContext对象:
classRequestContext(object): def__init__(self,app,environ,request=None): self.app=app#app对象 ifrequestisNone: request=app.request_class(environ) self.request=request#封装request self.url_adapter=app.create_url_adapter(self.request) self.flashes=None self.session=None#一开始的session
在这个对象中封装了session,最初为None。接着在wsgi_app中执行ctx.push:
defpush(self): app_ctx=_app_ctx_stack.top#获取app上下文 ifapp_ctxisNoneorapp_ctx.app!=self.app: app_ctx=self.app.app_context()#将app上下文push到app_ctx对于的LocalStack中 app_ctx.push() self._implicit_app_ctx_stack.append(app_ctx) else: self._implicit_app_ctx_stack.append(None) ifhasattr(sys,'exc_clear'): sys.exc_clear() _request_ctx_stack.push(self) ifself.sessionisNone:#判断session是否为None,一开始为None session_interface=self.app.session_interface#获取操作session的对象 self.session=session_interface.open_session(#调用open_session创建session self.app,self.request ) ifself.sessionisNone: self.session=session_interface.make_null_session(self.app)
这里我们主要关注session,前面的代码在上下文中已经进行了相关说明,这里有个判断session是否为None,刚开始RequestContext中的session为None,所以条件成立,此时执行以下语句:
session_interface=self.app.session_interface self.session=session_interface.open_session( self.app,self.request ) ifself.sessionisNone: self.session=session_interface.make_null_session(self.app)
首先来看session_interface=self.app.session_interface,self.app.session_interface就是app中的session_interface属性:
session_interface=SecureCookieSessionInterface()
默认是一个SecureCookieSessionInterface()对象,该对象的内部主要实现了open_session和save_session用于使用和保存session。接着self.session被重新赋值为session_interface.open_session(self.app,self.request)方法返回的值,以下为open_session源码:
defopen_session(self,app,request): s=self.get_signing_serializer(app)#根据app.secret_key获取签名算法 ifsisNone: returnNone #根据配置中的session_cookie_name获取session对于的值 val=request.cookies.get(app.session_cookie_name)#如果request.cookies为空,val为空 ifnotval: returnself.session_class() max_age=total_seconds(app.permanent_session_lifetime) try: data=s.loads(val,max_age=max_age) returnself.session_class(data) exceptBadSignature: returnself.session_class()
该方法返回self.session_class(),当请求第一次来时,request.cookies为None,所以val也为None,返回self.session_class(),而session_class又是SecureCookieSession:
session_class=SecureCookieSession
所以我们继续看SecureCookieSession:
classSecureCookieSession(CallbackDict,SessionMixin): accessed=False def__init__(self,initial=None): defon_update(self): self.modified=True self.accessed=True super(SecureCookieSession,self).__init__(initial,on_update) def__getitem__(self,key): self.accessed=True returnsuper(SecureCookieSession,self).__getitem__(key) defget(self,key,default=None): self.accessed=True returnsuper(SecureCookieSession,self).get(key,default) defsetdefault(self,key,default=None): self.accessed=True returnsuper(SecureCookieSession,self).setdefault(key,default)
该类继承了CallbackDict,SessionMixin我们继续来看看CallbackDict:
classCallbackDict(UpdateDictMixin,dict): """Adictthatcallsafunctionpassedeverytimesomethingischanged. Thefunctionispassedthedictinstance. """ def__init__(self,initial=None,on_update=None): dict.__init__(self,initialor()) self.on_update=on_update def__repr__(self): return'<%s%s>'%( self.__class__.__name__, dict.__repr__(self) )
也就是说SecureCookieSession继承了CallbackDict而CallbackDict继承了原生的dict,所以我们可以认为SecureCookieSession是一个特殊的字典,是调用了SecureCookieSessionInterface类中open_session返回的特殊字典,经过进一步分析self.session此时就是这个字典,这也意味着session在执行open_session方法时候被创建了,并保存在ctx中,也就是在RequestContext对象中,当我们使用session时候是通过全局变量session=LocalProxy(partial(_lookup_req_object,'session'))由LocalProxy对象从ctx中获取到session。
第二次请求
开始我们知道session第一次请求来的时候是在open_session方法之后被创建,当第二次请求时,此时在open_session方法中,val已经不在是None,此时获取cookie的有效时长,如果cookie依然有效,通过与写入时同样的签名算法将cookie中的值解密出来并写入字典并返回,若cookie已经失效,则仍然返回'空字典',这样以来在第二次请求中就能获取到之前保存的session数据。
session生命周期
我们介绍了session创建时候是在ctx.push时候开始创建,也就是说在这之后我们就可以使用session,对它进行操作了,那么session什么时候保存呢?我们接下来继续看wsgi_app:
defwsgi_app(self,environ,start_response): ctx=self.request_context(environ) error=None try: try: ctx.push() response=self.full_dispatch_request() exceptExceptionase: error=e response=self.handle_exception(e) except: error=sys.exc_info()[1] raise returnresponse(environ,start_response) finally: ifself.should_ignore_error(error): error=None ctx.auto_pop(error)
生成session后,接着执行self.full_dispatch_request():
deffull_dispatch_request(self): """Dispatchestherequestandontopofthatperformsrequest preandpostprocessingaswellasHTTPexceptioncatchingand errorhandling. ..versionadded::0.7 """ self.try_trigger_before_first_request_functions()#执行app.before_first_reques钩子函数 try: request_started.send(self)#触发request_started信号 rv=self.preprocess_request()#执行before_request钩子函数 ifrvisNone: rv=self.dispatch_request()#执行视图函数 exceptExceptionase: rv=self.handle_user_exception(e) returnself.finalize_request(rv)
这一部分先执行钩子app.before_first_reques在触发request_started信号,再执行before_request钩子函数,然后在执行视图函数,rv是执行完视图函数的返回值,最后执行finalize_request,这里的session保存就发生在这里:
deffinalize_request(self,rv,from_error_handler=False): response=self.make_response(rv) try: response=self.process_response(response) request_finished.send(self,response=response) exceptException: ifnotfrom_error_handler: raise self.logger.exception('Requestfinalizingfailedwithan' 'errorwhilehandlinganerror') returnresponse
注意这里的在最后会session判断是否为空,会执行save_session方法,也就是SecureCookieSessionInterface的save_session方法:
defsave_session(self,app,session,response): domain=self.get_cookie_domain(app) path=self.get_cookie_path(app) #Ifthesessionismodifiedtobeempty,removethecookie. #Ifthesessionisempty,returnwithoutsettingthecookie. ifnotsession: ifsession.modified: response.delete_cookie( app.session_cookie_name, domain=domain, path=path ) return #Adda"Vary:Cookie"headerifthesessionwasaccessedatall. ifsession.accessed: response.vary.add('Cookie') ifnotself.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 )
该方法最后保存的session调用的response.set_cookie,其实是将数据保存在cookie中,也就是在客户端的浏览器中,并非在服务端进行数据的保存,当请求完毕后会执行ctx.auto_pop(error)这时候会从上下文中将session和request删除,到此,session的生命周期结束。
视图函数使用session
在介绍flask的上下文中就已经对session进行过介绍,其本质也是通过LocalProxy操作上下文从而设置session,我们以session['username']='wd'作为列子,首先根据
session=LocalProxy(partial(_lookup_req_object,'session'))
session是一个LocalProxy对象,执行session['username']=‘wd'则执行LocalProxy对象的__setitem__方法,而__setitem__方法中则是调用_get_current_object获取ctx中的session对象,而其对象本质是一个特殊的字典,相当于在字典中加一对key,value。
小结
flask内置session本质上依靠上下文,当请求到来时,调用session_interface中的open_session方法解密获取session的字典,并保存在RequestContext.session中,也就是上下文中,然后在视图函数执行完毕后调用session_interface的save_session方法,将session以加密的方式写入response的cookie中,浏览器再保存数据。而第三方的session组件原理就是基于是open_session方法和save方法,从而实现session更多的session保存方案。
三、第三方组件flask-session
flask-session支持多种数据库session保存方案如:redis、memchached、mongodb甚至文件系统等。官方文档:https://pythonhosted.org/Flask-Session/
安装:
pip3installflask-session
redis
importredis fromflaskimportFlask,session fromflask_sessionimportSession fromdatetimeimporttimedelta app=Flask(__name__) app.debug=True app.secret_key='adavafa' app.config['SESSION_TYPE']='redis'#session类型为redis app.config['SESSION_PERMANENT']=True#如果设置为True,则关闭浏览器session就失效。 app.config['PERMANENT_SESSION_LIFETIME']=timedelta(seconds=20) #一个持久化的会话的生存时间,是一个datetime.timedelta对象,也可以用一个整数来表示秒,前提设置了PERMANENT_SESSION_LIFETIME为True app.config['SESSION_USE_SIGNER']=False#是否对发送到浏览器上session的cookie值进行加密,默认False app.config['SESSION_KEY_PREFIX']='flask-session'#保存到redis中的key的前缀 app.config['SESSION_COOKIE_NAME']='session_id'#保存在浏览器的cookie名称 app.config['SESSION_REDIS']=redis.Redis(host='10.1.210.33',port=‘6379',password=‘123123')#用于连接redis的配置 #其他配置,不经常使用 app.config['SESSION_COOKIE_DOMAIN']='127.0.0.1'#设置cookie的域名,不建议设置默认为server_name app.config['SESSION_COOKIE_PATH']='/'#会话cookie的路径。如果未设置,则cookie将对所有url有效,默认为'/' app.config['SESSION_COOKIE_HTTPONLY']=True#是否启动httponly,默认为true,为了防止xss脚本访问cookie Session(app) @app.route('/login') defindex(): session["username"]="jack" return'login' if__name__=='__main__': app.run()
Memchached
importmemcache fromflaskimportFlask,session fromflask_sessionimportSession fromdatetimeimporttimedelta app=Flask(__name__) app.debug=True app.secret_key='adavafa' app.config['SESSION_TYPE']=‘memcached'#session类型为memcached app.config['SESSION_PERMANENT']=True#如果设置为True,则关闭浏览器session就失效。 app.config['PERMANENT_SESSION_LIFETIME']=timedelta(seconds=20) #一个持久化的会话的生存时间,是一个datetime.timedelta对象,也可以用一个整数来表示秒,前提设置了PERMANENT_SESSION_LIFETIME为True app.config['SESSION_USE_SIGNER']=False#是否对发送到浏览器上session的cookie值进行加密,默认False app.config['SESSION_KEY_PREFIX']='flask-session'#保存到缓存中的key的前缀 app.config['SESSION_COOKIE_NAME']='session_id'#保存在浏览器的cookie名称 app.config['SESSION_MEMCACHED']=memcache.Client(['10.1.210.33:12000'])#连接 #其他配置,不经常使用 app.config['SESSION_COOKIE_DOMAIN']='127.0.0.1'#设置cookie的域名,不建议设置默认为server_name app.config['SESSION_COOKIE_PATH']='/'#会话cookie的路径。如果未设置,则cookie将对所有url有效,默认为'/' app.config['SESSION_COOKIE_HTTPONLY']=True#是否启动httponly,默认为true,为了防止xss脚本访问cookie Session(app) @app.route('/login') defindex(): session["username"]="jack" return'login' if__name__=='__main__': app.run()
Filesystem
fromflaskimportFlask,session fromflask_sessionimportSession fromdatetimeimporttimedelta app=Flask(__name__) app.debug=True app.secret_key='adavafa' app.config['SESSION_TYPE']='filesystem'#session类型为filesystem app.config['SESSION_FILE_DIR']='/opt/db'#文件保存目录 app.config['SESSION_FILE_THRESHOLD']=300#存储session的个数如果大于这个值时,开始删除 app.config['SESSION_PERMANENT']=True#如果设置为True,则关闭浏览器session就失效。 app.config['PERMANENT_SESSION_LIFETIME']=timedelta(seconds=20) #一个持久化的会话的生存时间,是一个datetime.timedelta对象,也可以用一个整数来表示秒,前提设置了PERMANENT_SESSION_LIFETIME为True app.config['SESSION_USE_SIGNER']=False#是否对发送到浏览器上session的cookie值进行加密,默认False app.config['SESSION_KEY_PREFIX']='flask-session'#保存到文件中的key的前缀 app.config['SESSION_COOKIE_NAME']='session_id'#保存在浏览器的cookie名称 #其他配置,不经常使用 app.config['SESSION_COOKIE_DOMAIN']='127.0.0.1'#设置cookie的域名,不建议设置默认为server_name app.config['SESSION_COOKIE_PATH']='/'#会话cookie的路径。如果未设置,则cookie将对所有url有效,默认为'/' app.config['SESSION_COOKIE_HTTPONLY']=True#是否启动httponly,默认为true,为了防止xss脚本访问cookie Session(app) @app.route('/login') defindex(): session["username"]="jack" return'login' if__name__=='__main__': app.run()
mongodb
importpymongo fromflaskimportFlask,session fromflask_sessionimportSession fromdatetimeimporttimedelta app=Flask(__name__) app.debug=True app.secret_key='adavafa' app.config['SESSION_TYPE']='mongodb'#session类型为mongodb app.config['SESSION_MONGODB']=pymongo.MongoClient('localhost',27017) app.config['SESSION_MONGODB_DB']='数据库名称' app.config['SESSION_MONGODB_COLLECT']='表名称' app.config['SESSION_PERMANENT']=True#如果设置为True,则关闭浏览器session就失效。 app.config['PERMANENT_SESSION_LIFETIME']=timedelta(seconds=20) #一个持久化的会话的生存时间,是一个datetime.timedelta对象,也可以用一个整数来表示秒,前提设置了PERMANENT_SESSION_LIFETIME为True app.config['SESSION_USE_SIGNER']=False#是否对发送到浏览器上session的cookie值进行加密,默认False app.config['SESSION_KEY_PREFIX']='flask-session'#保存的session的key的前缀 app.config['SESSION_COOKIE_NAME']='session_id'#保存在浏览器的cookie名称 #其他配置,不经常使用 app.config['SESSION_COOKIE_DOMAIN']='127.0.0.1'#设置cookie的域名,不建议设置默认为server_name app.config['SESSION_COOKIE_PATH']='/'#会话cookie的路径。如果未设置,则cookie将对所有url有效,默认为'/' app.config['SESSION_COOKIE_HTTPONLY']=True#是否启动httponly,默认为true,为了防止xss脚本访问cookie Session(app) @app.route('/login') defindex(): session["username"]="jack" return'login' if__name__=='__main__': app.run()
sqlalchemy
importredis fromflaskimportFlask,session fromflask_sessionimportSession fromflask_sqlalchemyimportSQLAlchemy app=Flask(__name__) app.debug=True app.secret_key='adavafa' #设置数据库链接 app.config['SQLALCHEMY_DATABASE_URI']='mysql+pymysql://root:dev@127.0.0.1:3306/devops?charset=utf8' app.config['SQLALCHEMY_TRACK_MODIFICATIONS']=True #实例化SQLAlchemy db=SQLAlchemy(app) app.config['SESSION_TYPE']='sqlalchemy'#session类型为sqlalchemy app.config['SESSION_SQLALCHEMY']=db#SQLAlchemy对象 app.config['SESSION_SQLALCHEMY_TABLE']='表名'#session要保存的表名称 app.config['SESSION_PERMANENT']=True#如果设置为True,则关闭浏览器session就失效。 app.config['PERMANENT_SESSION_LIFETIME']=timedelta(seconds=20) #一个持久化的会话的生存时间,是一个datetime.timedelta对象,也可以用一个整数来表示秒,前提设置了PERMANENT_SESSION_LIFETIME为True app.config['SESSION_USE_SIGNER']=False#是否对发送到浏览器上session的cookie值进行加密,默认False app.config['SESSION_KEY_PREFIX']='flask-session'#保存的session的key的前缀 app.config['SESSION_COOKIE_NAME']='session_id'#保存在浏览器的cookie名称 #其他配置,不经常使用 app.config['SESSION_COOKIE_DOMAIN']='127.0.0.1'#设置cookie的域名,不建议设置默认为server_name app.config['SESSION_COOKIE_PATH']='/'#会话cookie的路径。如果未设置,则cookie将对所有url有效,默认为'/' app.config['SESSION_COOKIE_HTTPONLY']=True#是否启动httponly,默认为true,为了防止xss脚本访问cookie Session(app) @app.route('/login') defindex(): session["username"]="jack" return'login' if__name__=='__main__': app.run() ###使用SQLAlchemy时候先确保数据库和表都存在 在命令行中创建表 #>>>fromappimportdb #>>>db.create_all()
原理
这里以redis作为session存储方案做分析,以下是RedisSessionInterface源码:
classRedisSessionInterface(SessionInterface): serializer=pickle session_class=RedisSession def__init__(self,redis,key_prefix,use_signer=False,permanent=True): ifredisisNone: fromredisimportRedis redis=Redis() self.redis=redis self.key_prefix=key_prefix self.use_signer=use_signer self.permanent=permanent defopen_session(self,app,request): sid=request.cookies.get(app.session_cookie_name) ifnotsid: sid=self._generate_sid() returnself.session_class(sid=sid,permanent=self.permanent) ifself.use_signer: signer=self._get_signer(app) ifsignerisNone: returnNone try: sid_as_bytes=signer.unsign(sid) sid=sid_as_bytes.decode() exceptBadSignature: sid=self._generate_sid() returnself.session_class(sid=sid,permanent=self.permanent) ifnotPY2andnotisinstance(sid,text_type): sid=sid.decode('utf-8','strict') val=self.redis.get(self.key_prefix+sid) ifvalisnotNone: try: data=self.serializer.loads(val) returnself.session_class(data,sid=sid) except: returnself.session_class(sid=sid,permanent=self.permanent) returnself.session_class(sid=sid,permanent=self.permanent) defsave_session(self,app,session,response): domain=self.get_cookie_domain(app) path=self.get_cookie_path(app) ifnotsession: ifsession.modified: self.redis.delete(self.key_prefix+session.sid) response.delete_cookie(app.session_cookie_name, domain=domain,path=path) return httponly=self.get_cookie_httponly(app) secure=self.get_cookie_secure(app) expires=self.get_expiration_time(app,session) val=self.serializer.dumps(dict(session)) self.redis.setex(name=self.key_prefix+session.sid,value=val, time=total_seconds(app.permanent_session_lifetime)) ifself.use_signer: session_id=self._get_signer(app).sign(want_bytes(session.sid)) else: session_id=session.sid response.set_cookie(app.session_cookie_name,session_id, expires=expires,httponly=httponly, domain=domain,path=path,secure=secure)
分析:RedisSessionInterface继承了SessionInterface
classSessionInterface(FlaskSessionInterface): def_generate_sid(self): returnstr(uuid4()) def_get_signer(self,app): ifnotapp.secret_key: returnNone returnSigner(app.secret_key,salt='flask-session', key_derivation='hmac')
而SessionInterface又继承了FlaskSessionInterface,而FlaskSessionInterface又继承了flask内置的SessionInterface,并且RedisSessionInterface重写了内置session的open_session和save_session.
首先是RedisSessionInterface实例化用于初始化配置,例如redis的连接、签名配置、过期配置、前缀配置等。
接下来看两个核心方法:open_session方法和save_session方法。
open_session:
defopen_session(self,app,request): sid=request.cookies.get(app.session_cookie_name)#获取sessionid ifnotsid:#判断sessionid是否为空,为空表示第一次请求 sid=self._generate_sid()#返回使用uuid4随机字符串 returnself.session_class(sid=sid,permanent=self.permanent) ifself.use_signer:#判断签名配置 signer=self._get_signer(app) ifsignerisNone: returnNone try: sid_as_bytes=signer.unsign(sid)#对sessionid进行加密签名 sid=sid_as_bytes.decode() exceptBadSignature: sid=self._generate_sid() returnself.session_class(sid=sid,permanent=self.permanent) ifnotPY2andnotisinstance(sid,text_type): sid=sid.decode('utf-8','strict') val=self.redis.get(self.key_prefix+sid)#获取seession数据 ifvalisnotNone: try: data=self.serializer.loads(val)#反序列化数据 returnself.session_class(data,sid=sid)#返回 except: returnself.session_class(sid=sid,permanent=self.permanent) returnself.session_class(sid=sid,permanent=self.permanent)
改方法先从cookie中获取sessionid,然后对sessionid判断是否为空,为空表示第一次请求,则通过self._generate_sid()返回随机字符串,作为返回给浏览器的sessionid
def_generate_sid(self): returnstr(uuid4())
接着判断签名判断是否为true,然后对session进行签名,这里和内置session不同的是获取session的时候通过self.redis.get(self.key_prefix+sid)在redis中进行获取。
save_session:
defsave_session(self,app,session,response): domain=self.get_cookie_domain(app)#获取cookie中的域名 path=self.get_cookie_path(app)#获取cookie中path ifnotsession:#判断有误session对象 ifsession.modified:#没有但是被修改了,表示已经被删除了 self.redis.delete(self.key_prefix+session.sid)#清空session response.delete_cookie(app.session_cookie_name, domain=domain,path=path) return httponly=self.get_cookie_httponly(app) secure=self.get_cookie_secure(app) expires=self.get_expiration_time(app,session) val=self.serializer.dumps(dict(session)) self.redis.setex(name=self.key_prefix+session.sid,value=val, time=total_seconds(app.permanent_session_lifetime))#保存session ifself.use_signer: session_id=self._get_signer(app).sign(want_bytes(session.sid)) else: session_id=session.sid response.set_cookie(app.session_cookie_name,session_id,#设置cookie expires=expires,httponly=httponly, domain=domain,path=path,secure=secure)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。