天天看点

python后台代码规范----登录会话(session)

Flask session 概念:

程序可以把数据存储在用户会话中,用户会话是-种私有存储,默认情况下,它会保存在客户端cookie中。Flask提供了session 对

象来操作用户会话。session 是基于cookie实现, 保存在服务端的键值对(形式为 {随机字符串:‘xxxxxx’}), 同时在浏览器中的cookie中也对应一相同的随机字符串,用来再次请求的 时候验证;

注意 :Flask中的session是存在浏览器中 默认key是session(加密的cookie),使用session时要设置一个密钥app. secret_ key

操作session就像操作python 中的字典一样,我们可以使用session[‘user’]获取值,也可以使用session. get(‘user’)获取值。

因为session就像字典一样所以,操作它的时候有两种方法:

(1)result = session[‘username’] :如果内容不存在,将会报异常

(2)result = session.get(‘username’) :如果内容不存在,将返回None(推荐用法)

所以,使用第二种方法获取session较好。

下面的代码中展示了session的设置、读取、删除、清除

from flask import Flask, session
from datetime import timedelta
import os
 
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)  # 配置7天有效
 
 
# 设置session
@app.route('/')
def set():
    session['username'] = 'zhangvalue'  # 设置“字典”键值对
    session.permanent = True  # 设置session的有效时间,长期有效,一个月的时间有效,
    # 具体看上面的配置时间具体的,没有上面设置的时间就是一个月有效
    print(app.config['SECRET_KEY'])
    return 'success'
 
 
# 读取session
@app.route('/get')
def get():
    # 第一种session获取如果不存在会报错
    # session['username']
    # 推荐使用session.get('username')
    # session.get('username')
    return session.get('username')
 
 
# 删除session
@app.route('/delete/')
def delete():
    print(session.get('username'), session.pop('username', None))
    # 或者 session['username'] = False
    print(session.get('username'))
    return 'success'
 
 
# 清除session中所有数据
@app.route('/clear')
def clear():
    print(session.get('username'))
    # 清除session中所有数据
    session.clear
    print(session.get('username'))
    return 'success'
 
 
if __name__ == '__main__':
    app.run(host='0.0.0.0', port='5010')
           

在获取session的时候如果如果使用第一种方法,不存在经就会报错,因为不存在key为username2

设置session的过期时间

如果没有指定session的过期时间,那么默认是浏览器关闭后就自动结束。

session.permanent = True在flask下则可以将有效期延长至一个月。下面有方法可以配置具体多少天的有效期。

如果没有指定session的过期时间,那么默认是浏览器关闭后就自动结束

如果设置了session的permanent属性为True,那么过期时间是31天。

可以通过给app.config设置PERMANENT_SESSION_LIFETIME来更改过期时间,这个值的数据类型是datetime.timedelay类型。

一种更先进的配置有效期的方法:(比如配置7天有效)

1.引入包:from datetime import timedelta

2.配置有效期限:app.config[‘PERMANENT_SESSION_LIFETIME’] = timedelta(days=7) # 配置7天有效

3.设置:session.permanent = True

再次认识session

flask的session是基于cookie的会话保持。简单的原理即:

当客户端进行第一次请求时,客户端的HTTP request(cookie为空)到服务端,服务端创建session,视图函数根据form表单填写session,请求结束时,session内容填写入response的cookie中并返回给客户端,客户端的cookie中便保存了用户的数据。

当同一客户端再次请求时, 客户端的HTTP request中cookie已经携带数据,视图函数根据cookie中值做相应操作(如已经携带用户名和密码就可以直接登陆)。

在 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'
 
 
@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 /,会返回该用户的名字。

flask中session使用非常简单,但是实现原理却没那么简单,下面我们通过几个问题来弄清楚session是如何实现的。

二、请求第一次来时,session是什么时候生成的?存放在哪里?

在flask学习笔记–请求上下文和应用上下文中已经知道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)
 
        if self.session is 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 有默认值:

执行SecureCookieSessionInterface.open_session()来生成默认session对象:

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

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 calls_update(name):
        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
        )
           

ave_session()比较简单,且有注释,便不再讲解,主要就是将session写入response.set_cookie中。这样便完成session的写入response工作,并由response返回至客户端。

再请求结束时会执行wsgi_app()的finally:ctx.auto_pop(error)函数,将与对应请求相关的request,session清除,session生命周期便结束。

总结:

至此,flask内置session的机制便讲解完毕,session的实现是依赖与flask的上下文管理,因此先弄清楚flask上下文,再来看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中

session.load()和session.get()区别

get先到缓存中去查如果没有就到DB中去查(即马上发出sql语句)总之如果你确定DB中有这个对象就用load()不确定就用get()这样效率会高