You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 22:35:23