为什么在 Django 中使用线程本地变量不好?
- 2025-03-20 08:48:00
- admin 原创
- 42
问题描述:
我使用线程局部变量来存储当前用户和请求对象。这样我就可以从程序中的任何位置(例如动态表单)轻松访问请求,而无需传递它们。
为了在中间件中实现线程本地存储,我遵循了 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 会带来巨大的风险。通过提供公开其他线程状态的数据存储,您为 Web 服务器中的一个线程提供了一种可能修改系统中另一个线程状态的方法。如果 threadlocal 数据包含用户描述或其他与身份验证相关的数据,则该数据可能被用作授予未经授权的用户访问权限或公开用户私人详细信息的攻击的基础。虽然可以构建一个可以免受此类攻击的 threadlocal 系统,但防御起来并构建一个不受任何此类漏洞影响的系统要容易得多。
我理解为什么全局变量可能不好,但在这种情况下,我在自己的服务器上运行自己的代码,所以我看不出两个全局变量会带来什么危险。
有人能解释一下其中涉及的安全问题吗?我问过很多人,如果他们读了这篇文章,知道我使用了线程局部变量,他们会如何破解我的应用程序,但没有人能告诉我。我开始怀疑这是喜欢显式传递对象的吹毛求疵的纯粹主义者的观点。
解决方案 1:
我完全不同意。TLS 非常有用。应该小心使用它,就像应该小心使用全局变量一样;但说它根本不应该使用就像说全局变量永远不应该使用一样荒谬。
例如,我将当前活动的请求存储在 TLS 中。这样就可以从我的日志类中访问它,而不必通过每个接口传递请求——包括许多根本不关心 Django 的接口。它允许我从代码中的任何地方创建日志条目;记录器输出到数据库表,如果在创建日志时请求恰好处于活动状态,它会记录诸如活动用户和所请求的内容之类的信息。
如果您不希望一个线程能够修改另一个线程的 TLS 数据,请设置您的 TLS 以禁止此操作,这可能需要使用本机 TLS 类。不过,我认为这个论点没有说服力;如果攻击者可以作为您的后端执行任意 Python 代码,您的系统已经受到致命威胁——例如,他可以对任何内容进行 monkey patch,以便稍后以其他用户身份运行。
显然,您需要在请求结束时清除任何 TLS;在 Django 中,这意味着在中间件类中的 process_response 和 process_exception 中清除它。
解决方案 2:
尽管您可能会混淆来自不同用户的数据,但应避免使用线程局部变量,因为它们会隐藏依赖关系。如果您将参数传递给方法,您会看到并知道您传递的是什么。但线程局部变量就像是后台的隐藏通道,您可能会想,在某些情况下,方法无法正常工作。
在某些情况下,线程本地变量是一个不错的选择,但是应该很少使用并且谨慎使用!
解决方案 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)
解决方案 4:
这个问题确实很老了,但是我刚刚看到有人提到它,所以我只想指出,这个问题引用的 wiki 页面在 2010 年停止推荐线程本地存储,然后在 2012 年被完全删除。
扫码咨询,免费领取项目管理大礼包!