web框架中的请求上下文

渗透技巧 2年前 (2022) admin
814 0 0

背景

最近在研究web框架时,对”请求上下文”这个基础概念有了更多的了解,因此记录一下,包括以下内容:

  • “请求上下文”是什么?
  • web框架(flask和gin)实现”请求上下文”的区别?
  • “线程私有数据”是什么?

学习过程

  • “请求上下文”是什么?

    根据 Go语言动手写Web框架 – Gee第二天 上下文Context[1]Context:请求控制器,让每个请求都在掌控之中[2] 两篇文章,可以知道从”框架开发者”的角度看,”请求上下文”包括:

    * 请求对象:包括请求方法、路径、请求头等内容
    * 响应对象:可以用来返回http响应
    * 工具函数:可以用来更方便地操作"请求对象""响应对象"

    那么web框架怎么让”框架的使用者”拿到”请求上下文”呢?

  • “框架的使用者怎么”拿到”请求上下文”?

    flask框架中请求上下文是一个全局变量,而gin框架中请求上下文被当作参数传递。

    根据flask文档[3]知道request对象包含有请求信息,可以如下获取

    from flask import request

    @app.route('/login', methods=['POST''GET'])
    def login():
        ...
        if request.method == 'POST':
            if valid_login(request.form['username'],
                           request.form['password']):

    根据gin文档[4]知道gin.Context实例c中包含有请求信息,可以如下获取

    router := gin.Default()

      router.GET("/welcome", func(c *gin.Context) {
       firstname := c.DefaultQuery("firstname""Guest")
       lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")

       c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
      })

    从上面的使用方法可以看出来,flask和gin框架实现”请求上下文”有一些区别:

    * gin框架中"框架使用者"需要把"请求上下文"当作参数,显示地传递
    * flask框架中"框架使用者"只需要request这个全局变量,就能获得"请求上下文"

    于是就有两个问题:

    * flask的request是个全局变量,那"基于多线程实现"的服务端同时收到多个请求时,request怎么能代表当前线程处理的请求呢?
    * flask似乎对"框架使用者"来说更方便,毕竟不需要多传递一个参数。那为什么gin框架不也这么设计呢?

    第一个问题其实涉及到”线程私有数据”的概念

线程私有数据

  • 是什么?

    举个例子,下面代码中新线程看不到主线程的mydata变量,因为mydata是”主线程”和”新线程”的私有数据”

    import threading
    from threading import local

    mydata = local()
    mydata.number = 42


    def f():
        if getattr(mydata, "number", None) is not None:
            print(mydata.number)    # 这里会打印42吗?


    thread = threading.Thread(target=f)
    thread.start()
    thread.join()
  • threading.local是怎么实现的?

    源码[5]中可以看到localdict是实际存放数据的对象,每个线程对应一个localdict。

    线程在读写”线程私有数据”时,会找到自己的localdict。

    class _localimpl:
      ...

      def get_dict(self):
          """Return the dict for the current thread. Raises KeyError if none
          defined."
    ""
          thread = current_thread()
          return self.dicts[id(thread)][1]    # id(thread)是当前线程对象内存地址,每个线程应该是唯一的

      def create_dict(self):
          """Create a new dict for the current thread, and return it."""
          localdict = {}
          key = self.key
          thread = current_thread()
          idt = id(thread)    # id(thread)是当前线程对象内存地址,每个线程应该是唯一的
          ...
          self.dicts[idt] = wrthread, localdict
          return localdict

      from threading import current_thread, RLock

    那flask框架是用了threading.local吗?

  • flask框架用了threading.local吗

    先说结论:flask的request对象不是基于”threading.local”,而是”contextvars.ContextVar”,后者可以实现”协程私有数据”。

    下面代码运行结果中,task1函数不会打印hello,所以可以看出来ContextVar是实现”协程私有数据”。

    from greenlet import greenlet
    from contextvars import ContextVar
    from greenlet import getcurrent as get_ident

    var = ContextVar("var")
    var.set("hello")


    def p(s):
        print(s, get_ident())

        try:
            print(var.get())
        except LookupError:
            pass


    def task1():
        p("task1")    # 不会打印hello
        # gr2.switch()


    # 测试ContextVar能否支持"协程私有数据"
    p("main")
    gr1 = greenlet(task1)
    gr1.switch()

    # 测试ContextVar能否支持"线程私有数据",结论是支持
    # import threading
    # p("main")
    # thread = threading.Thread(target=task1)
    # thread.start()
    # thread.join()

    flask/globals.py[6]中可以看到request是werkzeug库的Local类型

    _request_ctx_stack = LocalStack()
    ...
    request: "Request" = LocalProxy(partial(_lookup_req_object, "request"))  # type: ignore

    而从werkzeug/local.py源码[7] 可以看出来werkzeug库的Local是基于contextvars.ContextVar实现的

    class Local:
      ...

      def __init__(self) -> None:
          object.__setattr__(self, "_storage", ContextVar("local_storage")) # 基于contextvars.ContextVar

    所以,在”多线程”或者”多协程”环境下,flask的request全局变量能够代表当前线程或者协程处理的请求。

总结

web框架让”框架使用者”拿到”请求对象”有两种方式,包括”参数传递”、”全局变量”。

实现”全局变量”这种方式时,因为web服务可能是在多线程或者多协程的环境,所以需要每个线程或者协程使用”全局变量”时互不干扰,就涉及到”线程私有数据”的概念。

SpringWeb中在使用”RequestContextHolder.getRequestAttributes()静态方法”获取请求时,也是类似的业务逻辑。

参考资料

[1]

Go语言动手写Web框架 – Gee第二天 上下文Context: https://geektutu.com/post/gee-day2.html

[2]

Context:请求控制器,让每个请求都在掌控之中: https://time.geekbang.org/column/article/418283

[3]

flask文档: https://flask.palletsprojects.com/en/2.1.x/quickstart/#accessing-request-data

[4]

gin文档: https://pkg.go.dev/github.com/gin-gonic/gin#section-readme

[5]

源码: https://github.com/python/cpython/blob/main/Lib/_threading_local.py

[6]

flask/globals.py: https://github.com/pallets/flask/blob/main/src/flask/globals.py

[7]

werkzeug/local.py源码: https://github.com/pallets/werkzeug/blob/main/src/werkzeug/local.py

[8]

Go语言动手写Web框架 – Gee第二天 上下文Context: https://geektutu.com/post/gee-day2.html

[9]

flask 源码解析:上下文: https://cizixs.com/2017/01/13/flask-insight-context/


原文始发于微信公众号(leveryd):web框架中的请求上下文

版权声明:admin 发表于 2022年4月9日 下午11:42。
转载请注明:web框架中的请求上下文 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...