当前位置: 移动技术网 > IT编程>脚本编程>Python > flask seesion组件

flask seesion组件

2018年12月25日  | 移动技术网IT编程  | 我要评论

贵州考试,卡洛利娜库尔科娃,善田芳平书网

一、简介

    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框架会执行wsgi_app方法:
def wsgi_app(self, environ, start_response):
    ctx = self.request_context(environ)  #实例化生成requestcontext对象
    error = none 
    try:
        try:
            ctx.push()  #push上下文到localstack中
            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
        ctx.auto_pop(error)      # 删除localstack中的数据

在改方法中会生成一个ctx也就是requestcontext对象:

class requestcontext(object):
    def __init__(self, app, environ, request=none):
        self.app = app  # app对象
        if request is none:
            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:

def push(self):
        app_ctx = _app_ctx_stack.top  # 获取app上下文
        if app_ctx is none or app_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)
        if hasattr(sys, 'exc_clear'):
            sys.exc_clear()
        _request_ctx_stack.push(self)
        if self.session is none: # 判断session是否为none,一开始为none
            session_interface = self.app.session_interface # 获取操作session的对象
            self.session = session_interface.open_session( #调用open_session 创建session
                self.app, self.request
            )
            if self.session is none:
                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
            )
if self.session is none:
    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源码:

def open_session(self, app, request):
    s = self.get_signing_serializer(app) #根据app.secret_key获取签名算法
    if s is none:
        return none
    # 根据配置中的session_cookie_name获取session对于的值
    val = request.cookies.get(app.session_cookie_name)  #如果request.cookies为空,val为空
    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()

该方法返回self.session_class(),当请求第一次来时,request.cookies为none,所以val也为none,返回self.session_class(),而session_class又是securecookiesession:

session_class = securecookiesession

所以我们继续看securecookiesession:

class securecookiesession(callbackdict, sessionmixin):
    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)

该类继承了callbackdict, sessionmixin我们继续来看看callbackdict:

class callbackdict(updatedictmixin, dict):

    """a dict that calls a function passed every time something is changed.
    the function is passed the dict instance.
    """

    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)
        )

也就是说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:

def wsgi_app(self, environ, start_response):
    ctx = self.request_context(environ)
    error = none
    try:
        try:
            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
        ctx.auto_pop(error)

生成session后,接着执行self.full_dispatch_request():

def full_dispatch_request(self):
    """dispatches the request and on top of that performs request
    pre and postprocessing as well as http exception catching and
    error handling.

    .. 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钩子函数
        if rv is none:
            rv = self.dispatch_request() # 执行视图函数
    except exception as e:
        rv = self.handle_user_exception(e)
    return self.finalize_request(rv)

这一部分先执行钩子app.before_first_reques在触发request_started信号,再执行before_request钩子函数,然后在执行视图函数,rv是执行完视图函数的返回值,最后执行finalize_request,这里的session保存就发生在这里:

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

注意这里的在最后会session判断是否为空,会执行save_session方法,也就是securecookiesessioninterface的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
    )

该方法最后保存的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/

安装:

pip3 install flask-session

redis

import redis
from flask import flask, session
from flask_session import session
from datetime import timedelta

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')
def index():
    session["username"]="jack"
    return 'login'


if __name__ == '__main__':
    app.run()

memchached

import memcache
from flask import flask, session
from flask_session import session
from datetime import timedelta

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')
def index():
    session["username"]="jack"
    return 'login'


if __name__ == '__main__':
    app.run()

filesystem

from flask import flask, session
from flask_session import session
from datetime import timedelta

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')
def index():
    session["username"]="jack"
    return 'login'


if __name__ == '__main__':
    app.run()

mongodb

import pymongo
from flask import flask, session
from flask_session import session
from datetime import timedelta

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')
def index():
    session["username"]="jack"
    return 'login'


if __name__ == '__main__':
    app.run()

sqlalchemy

import redis
from flask import flask, session
from flask_session import session 
from flask_sqlalchemy import sqlalchemy

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')
def index():
    session["username"]="jack"
    return 'login'


if __name__ == '__main__':
    app.run()


###使用sqlalchemy时候先确保数据库和表都存在
在命令行中创建表
#>>> from app import db
#>>> db.create_all()

原理

这里以redis作为session存储方案做分析,以下是redissessioninterface源码:

class redissessioninterface(sessioninterface):
    serializer = pickle
    session_class = redissession

    def __init__(self, redis, key_prefix, use_signer=false, permanent=true):
        if redis is none:
            from redis import redis
            redis = redis()
        self.redis = redis
        self.key_prefix = key_prefix
        self.use_signer = use_signer
        self.permanent = permanent

    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, permanent=self.permanent)
        if self.use_signer:
            signer = self._get_signer(app)
            if signer is none:
                return none
            try:
                sid_as_bytes = signer.unsign(sid)
                sid = sid_as_bytes.decode()
            except badsignature:
                sid = self._generate_sid()
                return self.session_class(sid=sid, permanent=self.permanent)

        if not py2 and not isinstance(sid, text_type):
            sid = sid.decode('utf-8', 'strict')
        val = self.redis.get(self.key_prefix + sid)
        if val is not none:
            try:
                data = self.serializer.loads(val)
                return self.session_class(data, sid=sid)
            except:
                return self.session_class(sid=sid, permanent=self.permanent)
        return self.session_class(sid=sid, permanent=self.permanent)

    def save_session(self, app, session, response):
        domain = self.get_cookie_domain(app)
        path = self.get_cookie_path(app)
        if not session:
            if session.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))
        if self.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

分析:redissessioninterface继承了sessioninterface

class sessioninterface(flasksessioninterface):

    def _generate_sid(self):
        return str(uuid4())

    def _get_signer(self, app):
        if not app.secret_key:
            return none
        return signer(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:
def open_session(self, app, request):
    sid = request.cookies.get(app.session_cookie_name) #获取session id
    if not sid: #判断session id是否为空,为空表示第一次请求
        sid = self._generate_sid() # 返回使用uuid4随机字符串
        return self.session_class(sid=sid, permanent=self.permanent)
    if self.use_signer:# 判断签名配置
        signer = self._get_signer(app)
        if signer is none:
            return none
        try:
            sid_as_bytes = signer.unsign(sid) # 对session id 进行加密签名
            sid = sid_as_bytes.decode()
        except badsignature:
            sid = self._generate_sid()
            return self.session_class(sid=sid, permanent=self.permanent)

    if not py2 and not isinstance(sid, text_type):
        sid = sid.decode('utf-8', 'strict')
    val = self.redis.get(self.key_prefix + sid) # 获取seession数据
    if val is not none:
        try:
            data = self.serializer.loads(val) # 反序列化数据
            return self.session_class(data, sid=sid) #返回
        except:
            return self.session_class(sid=sid, permanent=self.permanent)
    return self.session_class(sid=sid, permanent=self.permanent)

改方法先从cookie中获取session id,然后对session id判断是否为空,为空表示第一次请求,则通过self._generate_sid()返回随机字符串,作为返回给浏览器的sessionid

def _generate_sid(self):
    return str(uuid4())

接着判断签名判断是否为true,然后对session 进行签名,这里和内置session不同的是获取session的时候通过self.redis.get(self.key_prefix + sid)在redis中进行获取。

save_session:

def save_session(self, app, session, response):
    domain = self.get_cookie_domain(app) # 获取cookie中的域名
    path = self.get_cookie_path(app)  # 获取cookie 中path
    if not session:  # 判断有误session对象
        if session.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
    if self.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)

 

 

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网