为什么在Django中使用线程局部变量不好?

60 投票
4 回答
23257 浏览
提问于 2025-04-16 01:09

我在使用线程本地存储来保存当前用户和请求对象。这样,我可以在程序的任何地方轻松访问请求(比如动态表单),而不需要到处传递这些对象。

为了在中间件中实现线程本地存储,我参考了Django网站上的一个教程:https://web.archive.org/web/20091128195932/http://code.djangoproject.com:80/wiki/CookBookThreadlocalsAndUser

不过,这个文档后来被修改了,建议避免使用这种技术:https://web.archive.org/web/20110504132459/http://code.djangoproject.com/wiki/CookBookThreadlocalsAndUser

文章中提到:

从设计的角度来看,线程本地存储本质上就是全局变量,容易出现全局变量通常会遇到的可移植性和可预测性问题。

更重要的是,从安全的角度来看,线程本地存储带来了巨大的风险。因为它提供了一种数据存储方式,可以暴露其他线程的状态,这样就可能让你的网站服务器中的一个线程修改系统中另一个线程的状态。如果线程本地存储的数据包含用户描述或其他与身份验证相关的数据,这些数据可能会被用作攻击的基础,从而让未授权的用户获得访问权限,或者暴露用户的私人信息。虽然可以构建一个安全的线程本地存储系统来防止这种攻击,但一开始构建一个不容易受到这种漏洞影响的系统要简单得多。

我明白为什么全局变量可能不好,但在这种情况下,我是在自己的服务器上运行自己的代码,所以我看不出两个全局变量会带来什么危险。

有人能解释一下涉及的安全问题吗?我问过很多人,如果他们读了这篇文章并知道我在使用线程本地存储,他们会如何攻击我的应用,但没有人能告诉我。我开始怀疑这只是一些喜欢过于挑剔的纯粹主义者的看法,他们喜欢明确地传递对象。

4 个回答

13

这是一个关于如何创建与最新的Django 1.10兼容的TLS中间件的简单示例:

# coding: utf-8
# Copyright (c) Alexandre Syenchuk (alexpirine), 2016

try:
    from threading import local
except ImportError:
    from django.utils._threading_local import local

_thread_locals = local()

def get_current_request():
    return getattr(_thread_locals, 'request', None)

def get_current_user():
    request = get_current_request()
    if request:
        return getattr(request, 'user', None)

class ThreadLocalMiddleware(object):
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        _thread_locals.request = request
        return self.get_response(request)
15

虽然你可以把不同用户的数据混合在一起,但最好避免使用线程局部变量,因为它们会隐藏一些依赖关系。如果你把参数传给一个方法,你能看到并知道自己传了什么。但是线程局部变量就像是后台的一个隐藏通道,你可能会发现某些情况下这个方法的表现不太正常。

确实有一些情况使用线程局部变量是个不错的选择,但应该很少使用,并且要非常小心!

63

我完全不同意这个观点。TLS(线程局部存储)非常有用。使用它的时候要小心,就像使用全局变量时也要小心一样;但是说根本不应该使用TLS,这和说全局变量永远不能用是一样荒谬的。

举个例子,我会把当前活跃的请求存储在TLS中。这样我就可以在我的日志类中访问这个请求,而不需要在每一个接口中都传递这个请求——包括那些根本不关心Django的接口。这样我可以在代码的任何地方记录日志;日志记录器会把信息输出到数据库表中,如果在记录日志的时候有请求正在进行,它会记录像活跃用户和请求内容这样的信息。

如果你不想让一个线程修改另一个线程的TLS数据,那就设置你的TLS来禁止这种操作,这可能需要使用一个原生的TLS类。不过我觉得这个理由并不太说得通;如果攻击者能够在你的后端执行任意的Python代码,你的系统已经严重被攻破了——比如说,他可以修改任何东西,让它在之后以不同的用户身份运行。

显然,你会想在请求结束时清除任何TLS数据;在Django中,这意味着在中间件类的process_response和process_exception中清除它。

撰写回答