Web框架的请求上下文

VSole2022-04-21 16:54:48

背景

最近在研究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")) 

所以,flask并没有用threading.local,而是werkzeug库的Local类型。也因此在"多线程"或者"多协程"环境下,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]flask 源码解析:上下文:

https://cizixs.com/2017/01/13/flask-insight-context/

flask上下文
本作品采用《CC 协议》,转载必须注明作者和本文链接
Web框架的请求上下文
2022-04-21 16:54:48
最近在研究web框架时,对"请求上下文"这个基础概念有了更多的了解,因此记录一下,包括以下内容: "请求上下文"是什么? web框架(flask和gin)实现"请求上下文"的区别? "线程私有数据"是什么? 学习过程 "请求上下文"是什么? 根据 Go语言动手写Web框架 - Gee第二天 上下文Context[1] 和 Context:请求控制器,让每个请求都在掌控之中[2] 两篇文章
再看寻找Python SSTI攻击载荷的过程。获取基本类,此外,在引入了Flask/Jinja的相关模块后还可以通过
跨语言移植一直是技术领域内难以解决的问题,需要解决语言之间的约束,好在先前我们成功使用 Go 实现了 IIOP 协议通信,有了前车之鉴,所以这次我们将继续使用跨语言方式实现 Flask Session 伪造。本文以 Apache Superset 权限绕过漏洞(CVE-2023-27524) 为例讲述我们是如何在 Go 中实现 Flask 框架的 Session 验证、生成功能的。
Commander是一款功能强大的命令与控制C2服务器框架,在该工具的帮助下,广大红队和蓝队研究人员可以轻松部署自己的C2组件。
借助SecureX,您可以通过无缝集成SecureX威胁响应和您现有的安全技术来加速威胁搜寻和事件响应。无论是内置,预打包或自定义的集成,您都可以灵活地将您的工具组合在一起。如果您有Cisco Stealthwatch,Firepower,A...
sql注入已经出世很多年了,对于sql注入的概念和原理很多人应该是相当清楚了,SSTI也是注入类的漏洞,其成因其实是可以类比于sql注入的。BladeBlade 是 Laravel 提供的一个既简单又强大的模板引擎。它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
shad0w原理分析 part 1
2021-10-18 16:17:10
shad0w原理分析!
虽然市面上关于SSTI的题大都出在python上,但是这种攻击方式请不要认为只存在于 Python 中,凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言。
loguru 是一个 Python 简易且强大的第三方日志记录库,该库旨在通过添加一系列有用的功能来解决标准记录器的注意事项,从而减少 Python 日志记录的痛苦。
恶意软件会利用用户的信任进行传播,VirusTotal 利用海量数据总结了四种在恶意软件中常见的信任滥用方式。
VSole
网络安全专家