Flask源码剖析-session实现原理

1 cookie和session

http是无状态的,对于一些场景,比如购物网站,加购物车,付款这些过程需要记录前一个页面的状态,假如你选了一购物车的货物,准备去付款页面付款,此时你的商品信息全部都没有了,这是行不通的。为了保持客户端与服务器之间的会话状态,就有了cookie和session。
不严谨的说,cookie是存储在浏览器端的一些信息,session是存储在服务器端的信息,session存储后会生成一个sessionID,浏览器通过cookie把sessionID发送到服务器,服务器判断这个sessionID里面存储的信息,把信息给这个浏览器,如果浏览器没有sessionID,那么访问服务器后,服务器会为这个浏览器新建一个sessionID。
本文章的重点不是剖析session和cookie,而是研究一下flask的session是怎么实现的。

2 flask中使用session

from flask import Flask, session


app = Flask(__name__)
#RuntimeError: The session is unavailable because no secret key was set.  
#Set the secret_key on the application to something unique and secret.
app.config['SECRET_KEY'] = 'secret key xxx'

@app.route('/set_session')
def set_session():
    session['key'] = 'value'
    return "set_cookie_success"


@app.route('/get_session')
def get_session():
    try:
        session_value = session['key']
    except KeyError:
        session_value = None
    if session_value:
        return session_value
    else:
        return 'session key not found'


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

在这里插入图片描述
在这里插入图片描述
通过session可以在不同的请求之间共享数据,上面图片所示就是写入session和获取session的过程

带着几个问题:

  • 为什么session可以像字典一样操作?
  • 使用session为什么需要secret key?
  • 通过开发者工具会发现session出现在了浏览器的cookie中,为什么?

在这里插入图片描述

3 剖析flask内部session实现原理

"""
===============================step1==================================
从from flask import session开始
"""
# globals.py中session是一个全局变量,并且也是一个本地代理对象
session: "SessionMixin" = LocalProxy(  # type: ignore
    partial(_lookup_req_object, "session")
)
"""
===============================step2==================================
请求刚进来,ctx.py模块中的class RequestContext:中定义了session
"""
class RequestContext:
    def __init__(
        self,
        app: "Flask",
        environ: dict,
        request: t.Optional["Request"] = None,
        session: t.Optional["SessionMixin"] = None,) # session被初始化为None


"""
===============================step3==================================
在RequestContext对象被push到_request_ctx_stack中后
"""
# RequestContext类的push方法如下:
	def push(self)
		...
				# 请求刚进来的时候session就是None
        if self.session is None:
        		# 代码执行到这,这里定义了session的接口,很巧妙,只要session_interface 类
        		# 执行open_session方法就可以
           # 跳转到Flask类中
           """
           ===================step3.1==================
           self.app 是Flask类的实例
           在app.py模块中Flask类中有下面的代码
           session_interface = SecureCookieSessionInterface()
           session_interface 就是SecureCookieSessionInterface类的一个实例
           """
            session_interface = self.app.session_interface
           """
           ===================step3.2==================
           调用SecureCookieSessionInterface类的open_session方法,将app对象和request对象作为参数传递
           """
            self.session = session_interface.open_session(self.app, self.request)

            if self.session is None:
                self.session = session_interface.make_null_session(self.app)
		...



3.1 重点分析SecureCookieSessionInterface类

class SecureCookieSessionInterface(SessionInterface):
    """通过签名cookie方式存储session,依赖itsdangerous模块"""
    #: 用于加密session的“盐值”
    salt = "cookie-session"
    #: 签名算法,默认使用sha1
    digest_method = staticmethod(hashlib.sha1)
    #: the name of the itsdangerous supported key derivation.  The default is hmac.
    key_derivation = "hmac"
    #: A python serializer for the payload.  The default is a compact
    #: JSON derived serializer with support for some extra Python types
    #: such as datetime objects or tuples.
    serializer = session_json_serializer
    session_class = SecureCookieSession

    def get_signing_serializer(
        self, app: "Flask"
    ) -> t.Optional[URLSafeTimedSerializer]:
        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,
        )

    def open_session(
        self, app: "Flask", request: "Request"
    ) -> t.Optional[SecureCookieSession]:
"""
===============================step4=============================
open_session最后返回的是session_class,也就是说在请求被推进上下文的时候,创建了一个session_class
对象
session_class = SecureCookieSession

"""
        s = self.get_signing_serializer(app)
        if s is None:
            return None
        val = request.cookies.get(self.get_cookie_name(app))
        if not val:
            return self.session_class()
        max_age = int(app.permanent_session_lifetime.total_seconds())
        try:
            data = s.loads(val, max_age=max_age)
            return self.session_class(data)
        except BadSignature:
            return self.session_class()
"""
===============================step5=============================
看SecureCookieSession是个什么
class SecureCookieSession(CallbackDict, SessionMixin):
class CallbackDict(UpdateDictMixin, dict):
class UpdateDictMixin(dict):
没错SecureCookieSession就是继承了字典,所以session具有类似字典的功能
"""
    def save_session(
        self, app: "Flask", session: SessionMixin, response: "Response"
    ) -> None:
        name = self.get_cookie_name(app)
        domain = self.get_cookie_domain(app)
        path = self.get_cookie_path(app)
        secure = self.get_cookie_secure(app)
        samesite = self.get_cookie_samesite(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(
                    name, domain=domain, path=path, secure=secure, samesite=samesite
                )

            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)
        expires = self.get_expiration_time(app, session)
        """
        这句是关键,将session转换成字段后进行序列化
        """
        val = self.get_signing_serializer(app).dumps(dict(session))  # type: ignore
        """
        最后通过cookie发送到客户端
        """
        response.set_cookie(
            name,
            val,  # type: ignore
            expires=expires,
            httponly=httponly,
            domain=domain,
            path=path,
            secure=secure,
            samesite=samesite,
        )

3.2 请求进来后,开始准备响应

        ctx = self.request_context(environ)
        error: t.Optional[BaseException] = None
        try:
            try:
                ctx.push()
                # 主要看这句
                response = self.full_dispatch_request()

"""
===============================step6=============================
response = self.full_dispatch_request()

"""
def full_dispatch_request(self) -> Response:
    return self.finalize_request(rv)

def finalize_request(
        self,
        rv: t.Union[ResponseReturnValue, HTTPException],
        from_error_handler: bool = False,
    ) -> Response:
        response = self.make_response(rv)
        try:
            response = self.process_response(response)

def process_response(self, response: Response) -> Response:
    if not self.session_interface.is_null_session(ctx.session):
        self.session_interface.save_session(self, ctx.session, response)

"""
===============================step7=============================
又回到了save_session
"""

4 小结

简单总结一下,session和request一样,都是请求上下文对象的一个属性。
当请求进来后,session开辟一个继承自字典的对象,用于保存信息,处理请求过程中,更新session对象的内容,最后通过cookie的方式发送到浏览器,说通俗一点,flask是通过加密cookie来保存session信息的。
只要满足SessionInterface接口的类就可以用于flask的session处理,虽然我不能完全理解所有细节,但是源码写的是真的妙

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kobe_OKOK_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值