如何在Tornado/Streamlit中安全实现Django只读用户认证?
我需要将基于Tornado的Streamlit应用通过<iframe>集成到Django项目中,要求仅允许同一域名下已通过Django认证的用户访问Streamlit服务(支持Nginx部署或分端口运行)。
目前的实现依赖DJANGO_ALLOW_ASYNC_UNSAFE环境变量绕过异步安全检查,但担心并发访问会导致数据库损坏。虽已通过设置数据库只读权限临时规避风险,但怀疑Django的SessionStore等类并非线程安全(不确定是否具备进程安全性),希望找到一种安全的方式在Tornado这类异步框架中实现基于Django的只读认证。
禁用DJANGO_ALLOW_ASYNC_UNSAFE会直接触发异步安全错误。
import os os.environ['DJANGO_ALLOW_ASYNC_UNSAFE'] = 'true' import asyncio from importlib import import_module from typing import Optional, Awaitable import django django.setup() from django.conf import settings from django.http import HttpRequest from django.contrib.auth.middleware import get_user import tornado.web from tornado.httpserver import HTTPServer SessionStore = import_module(settings.SESSION_ENGINE).SessionStore class UnauthorizedHandler(tornado.web.RequestHandler): def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]: pass def prepare(self): return self.send_error(401) class AuthorizedHandler(tornado.web.RequestHandler): def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]: pass def get(self): return self.write("authorized") def get_user_from_django(request): session_key = request.cookies.get(settings.SESSION_COOKIE_NAME) if session_key is None: return None session_key = session_key.value django_request = HttpRequest() django_request.session = SessionStore(session_key) return get_user(django_request) def django_authentication_middleware(request): global unauthorized_application, authorized_application user = get_user_from_django(request) if user is not None and user.is_authenticated: return authorized_application(request) else: return unauthorized_application(request) unauthorized_application = tornado.web.Application([ (r".*", UnauthorizedHandler), ]) authorized_application = tornado.web.Application([ (r".*", AuthorizedHandler), ]) async def main(): server = HTTPServer(django_authentication_middleware) server.listen(8888) await asyncio.Event().wait() if __name__ == "__main__": asyncio.run(main())
1. 用线程池隔离Django同步认证逻辑
Tornado的异步协程中直接调用Django同步API会触发安全警告,因为Django核心组件(ORM、会话系统)未针对异步设计。将Django认证的同步代码放到线程池中执行,可避免阻塞事件循环,同时保证同步代码的线程安全:
修改后的关键代码片段
# 替换原get_user_from_django函数 async def get_user_from_django_async(request): session_key = request.cookies.get(settings.SESSION_COOKIE_NAME) if session_key is None: return None session_key = session_key.value # 在线程池中执行Django同步操作 def sync_get_user(): django_request = HttpRequest() django_request.session = SessionStore(session_key) return get_user(django_request) # 使用Tornado默认线程池执行同步逻辑 return await tornado.ioloop.IOLoop.current().run_in_executor(None, sync_get_user) # 异步版本的认证中间件 async def django_authentication_middleware(request): global unauthorized_application, authorized_application user = await get_user_from_django_async(request) if user is not None and user.is_authenticated: return authorized_application(request) else: return unauthorized_application(request)
此方案将所有Django同步操作隔离到单独线程,既不阻塞Tornado的异步事件循环,也规避了DJANGO_ALLOW_ASYNC_UNSAFE的风险,同时单线程内执行同步代码保证了线程安全。
2. 通过Nginx反向代理+Django认证端点验证
将Tornado/Streamlit服务置于Nginx之后,所有请求先转发到Django的只读认证端点验证会话有效性,Nginx根据返回结果决定是否放行:
Django端认证视图
from django.http import HttpResponse from django.contrib.auth.decorators import login_required @login_required def check_auth(request): # 仅验证用户认证状态,无写操作 return HttpResponse(status=200)
Nginx核心配置
# Streamlit请求入口 location /streamlit/ { auth_request /api/check-auth/; # 先请求Django认证端点 proxy_pass http://localhost:8888/; proxy_set_header Cookie $http_cookie; # 传递用户会话Cookie proxy_set_header X-Real-IP $remote_addr; } # Django认证端点转发 location = /api/check-auth/ { proxy_pass http://localhost:8000/; proxy_set_header Cookie $http_cookie; proxy_set_header X-Real-IP $remote_addr; proxy_pass_request_body off; # 无需传递请求体 proxy_set_header Content-Length ""; }
此方案将认证逻辑完全交给Django处理,Tornado无需直接调用Django内部API,彻底避免异步/同步冲突,且Nginx的反向代理拦截未认证请求,安全性更高。
3. 使用进程安全的会话存储后端
若使用Redis等进程安全的会话存储(如django-redis),可避免多进程/线程并发访问会话的安全问题:
- 配置Django使用Redis会话引擎:
# settings.py SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_CACHE_ALIAS = "redis"
Redis本身是线程安全的,且django-redis的会话实现基于原子操作,比数据库会话后端更适合多进程场景,即使Tornado多进程运行,也不会出现会话数据的并发问题。
内容的提问来源于stack exchange,提问作者Herbert

