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

Posted

技术标签:

【中文标题】为啥在 Django 中使用线程局部变量不好?【英文标题】:Why is using thread locals in Django bad?为什么在 Django 中使用线程局部变量不好? 【发布时间】:2011-03-14 17:17:06 【问题描述】:

我正在使用线程本地来存储当前用户和请求对象。这样我就可以从程序中的任何位置(例如动态表单)轻松访问请求,而无需传递它们。

为了在中间件中实现线程本地存储,我遵循了 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

来自文章:

从设计的角度来看,threadlocals 本质上是全局变量,并且受到全局变量通常带来的所有常见的可移植性和可预测性问题的影响。

更重要的是,从安全的角度来看,threadlocals 带来了巨大的风险。通过提供暴露其他线程状态的数据存储,您可以为 Web 服务器中的一个线程提供一种可能修改系统中另一个线程状态的方法。如果线程本地数据包含用户描述或其他与身份验证相关的数据,则该数据可用作攻击的基础,从而授予未经授权的用户访问权限,或公开用户的私人详细信息。虽然可以构建一个不受此类攻击的线程本地系统,但防御并构建一个从一开始就不受任何此类漏洞影响的系统要容易得多。

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

有人可以解释所涉及的安全问题吗?我问过很多人,如果他们阅读了这篇文章并且知道我正在使用线程本地,他们将如何破解我的应用程序,但没有人能够告诉我。我开始怀疑这是喜欢明确传递对象的纯粹主义者的观点。

【问题讨论】:

顺便问一下 - 你有原始示例吗?它现在已被删除,我想使用它... 这个sn-p与被删除的页面非常相似:djangosnippets.org/snippets/2179 GlobalRequest 中间件:djangosnippets.org/snippets/2853 【参考方案1】:

我完全不同意。 TLS 非常有用。它应该小心使用,就像全局变量应该小心使用一样;但是说根本不应该使用它就像说永远不应该使用全局变量一样荒谬。

例如,我将当前活动的请求存储在 TLS 中。这使得它可以从我的日志记录类中访问,而不必通过每个接口传递请求——包括许多根本不关心 Django 的接口。它让我可以从代码中的任何地方创建日志条目;记录器输出到数据库表,如果在生成日志时请求恰好处于活动状态,它会记录活动用户和请求的内容等内容。

如果您不希望一个线程具有修改另一个线程的 TLS 数据的能力,则将您的 TLS 设置为禁止此操作,这可能需要使用本机 TLS 类。不过,我觉得这个论点没有说服力。如果攻击者可以执行任意 Python 代码作为您的后端,那么您的系统已经受到了致命的威胁——例如,他可以修补任何东西,以便以后以不同的用户身份运行。

显然,您需要在请求结束时清除所有 TLS;在 Django 中,这意味着在中间件类的 process_response 和 process_exception 中清除它。

【讨论】:

感谢您的确认。我现在感觉不那么疯狂了=)。如果攻击者可以读取线程本地数据,那么他无论如何都必须能够通过 SSH 连接到我的机器。 不一定是 SSH,但至少对 Python 后端有某种控制。无论如何,整个论点似乎很做作。 我很好奇,您所说的“在请求结束时清除任何 TLS”究竟是什么意思。怎么清除?删除local 对象本身,还是仅删除您存储会话的local 对象上的属性?不过,老实说,我什至不确定它们之间是否存在相关差异。 @CoreDumpError 删除数据即可。如果您删除该对象,您将清除整个数据存储空间,而其他线程可能仍在使用这些数据存储空间。 一个巨大的警告 - 我发现你如何导入你的模块(绝对与相对)会影响模块是否会再次加载(使用它自己的一组全局变量) - 导致非常微妙的错误,尤其是在全局变量和 threading.local 周围! IE。 from path.to.package import global_event 将导致 from .package import global_event 的不同实例【参考方案2】:

尽管您可能会混淆来自不同用户的数据,但应避免使用线程局部变量,因为它们隐藏了依赖关系。如果您将参数传递给您看到并知道您传递的方法的方法。但是本地线程类似于后台的隐藏通道,您可能想知道,在某些情况下某个方法无法正常工作。

在某些情况下,线程局部变量是一个不错的选择,但应谨慎使用它们!

【讨论】:

但这不是问题,因为我知道在我调用 get_current_user() 的任何地方都会使用线程本地变量。无论如何,我在线程本地变量中只有两个变量。【参考方案3】:

关于如何创建与最新 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)

【讨论】:

这在使用 UWSGI 时不起作用。 local() 已共享 您能否进一步解释一下,@JavierBuzzi?你的意思是local()在使用UWSGI时是跨线程共享的?你有解释为什么会这样吗? 由于 UWSGI 的工作方式,它在所有线程之间共享所有内容以加快速度。 pythonanywhere.com/forums/topic/710——它很容易测试:pip install uwsgi 并自己测试它——作为旁注,我必须创建一个中间件,在请求开始时创建缓存并在请求结束时将其清除请求..只是为了确保 - 否则我在 uwsgi 中运行它时会遇到各种错误 threading.local 和 Django 与 uwsgi 完美配合。 Django 框架本身在内部使用它。我亲自用 Django 1.11 和 uwsgi 2.0.17 对其进行了测试。当然,对于敏感数据,您必须知道自己在做什么并进行很好的测试。因为 Django 正在运行同步,所以一个请求意味着一个线程。因为它使用线程池清理 threading.local() 必须在请求结束时完成。这是一个设计问题,因为您使用全局变量紧密耦合组件,但除此之外它还有效。 @SebastianBrestin 听起来不错,如果您查看 uwsgi 代码,您会发现它们修补了线程。【参考方案4】:

这个问题真的很老了,但我只是看到有人提到它,所以我只想注意这个问题引用的wiki页面stopped recommending threadlocal storage in 2010然后was deleted altogether by 2012。

【讨论】:

很公平......这让我想知道如何解决在 model.save() 中知道 request.user 的长期问题??

以上是关于为啥在 Django 中使用线程局部变量不好?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的线程函数中的局部变量会被其他线程中断?

为啥局部变量在 Java 中是线程安全的

为啥在 lambda 表达式中使用迭代变量不好

在sql查询中使用局部变量作为开关是不好的做法

Java中线程局部变量ThreadLocal使用教程及源码分析

静态局部变量是不好的做法吗?