
marimo FastAPI 鉴权实战用纯 ASGI 中间件把用户身份传入 Notebook【免费下载链接】marimoA reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. All in a modern, AI-native editor.项目地址: https://gitcode.com/GitHub_Trending/ma/marimo本文以官方示例 examples/frameworks/fastapi-auth 为主线讲解如何在 FastAPI 中为 marimo notebook 实现登录/登出与会话管理并通过mo.app_meta().request.user与mo.app_meta().request.meta把已认证用户信息传递进 notebook 单元格。读完后你可以复制一套可运行的鉴权中间件同时覆盖 HTTP 与 WebSocket 连接并理解 marimo 是如何在 ASGIscope层面消费user/meta的从而在自己的 FastAPI 应用中安全地为 marimo 应用做身份控制。示例场景与文件构成官方示例位于examples/frameworks/fastapi-auth/包含三个文件README.md说明推荐模式与运行方式main.pyFastAPI 应用 鉴权中间件 登录页notebook.py被挂载的 marimo notebook读取并展示用户信息示例覆盖的能力包括基于会话 Cookie 的登录 / 登出一个纯 ASGI 中间件为 HTTP 和 WebSocket 连接同时设置scope[user]与scope[meta]一个通过mo.app_meta().request读取用户信息的 marimo notebook。为什么必须用纯 ASGI 中间件这是整个示例中最关键的设计决策也是 README 专门用一节解释的原因marimo 使用 WebSocket 做实时通信。Starlette 的BaseHTTPMiddleware只处理 HTTP 请求因此在那里设置的scope[user]在 WebSocket 连接上是不可见的。纯 ASGI 中间件则能同时处理两者。从源码结构看这一点确实成立marimo 内部多处依赖scope[user]作为开发者约定的身份载体。例如内置的 ProxyMiddleware 判断请求是否已认证时直接读取scope.get(user)并检查is_authenticated属性marimo/_server/api/auth.py 中的CustomAuthenticationMiddleware甚至会显式保存并还原开发者提前写入的scope[user]KEY _marimo_prev_user以避免 Starlette 的AuthenticationMiddleware覆盖它。也就是说scope[user]/scope[meta]是 marimo 公开约定的 ASGI 接口——如果你的中间件只对 HTTP 生效notebook 在 WebSocket 握手后重新拉取请求上下文时就会拿不到身份信息。main.py 逐段解析一个可运行的鉴权骨架依赖声明PEP 723 内联元数据main.py 文件头部用# /// script块声明了依赖这是 uv 的 PEP 723 内联脚本元数据格式使得uv run --no-project main.py无需项目级pyproject.toml即可自动建环境并安装依赖# /// script # requires-python 3.12 # dependencies [ # fastapi, # marimo, # starlette, # uvicorn, # itsdangerous, # python-multipart, # ] # ///其中itsdangerous是 StarletteSessionMiddleware做 Cookie 签名所需的python-multipart则用于表单解析。AuthMiddleware覆盖 HTTP 与 WebSocket 的纯 ASGI 中间件核心实现见 AuthMiddleware完整逻辑如下class AuthMiddleware: # Paths that dont require authentication PUBLIC_PATHS {/login} def __init__(self, app): self.app app async def __call__(self, scope, receive, send): if scope[type] not in (http, websocket): await self.app(scope, receive, send) return # SessionMiddleware has already run, so scope[session] is available. session scope.get(session, {}) username session.get(username) if username: # Set user/meta so marimo can read them via mo.app_meta().request scope[user] { is_authenticated: True, username: username, } scope[meta] {role: admin} await self.app(scope, receive, send) return # Not logged in — block unauthenticated access. path scope.get(path, ) # Allow public paths through without authentication. if path in self.PUBLIC_PATHS: await self.app(scope, receive, send) return # Reject unauthenticated WebSocket connections. if scope[type] websocket: from starlette.websockets import WebSocket ws WebSocket(scope, receive, send) await ws.close(code4003) return # Redirect unauthenticated HTTP requests to /login. response Response( status_code302, headers{location: /login} ) await response(scope, receive, send)各分支的处理策略值得注意只拦截http与websocketlifespan等其他 ASGI 事件直接透传已登录从scope[session]取用户名写入scope[user]含is_authenticated与username两个键这正是 marimo 侧约定的可序列化结构下文详述和scope[meta]自定义数据例如{role: admin}未登录且命中PUBLIC_PATHS如/login放行未登录的 WebSocket直接以状态码4003关闭连接——不能像 HTTP 那样 302 跳转未登录的 HTTP返回 302 重定向到/login。代码注释中还给出两条实用建议不要用BaseHTTPMiddleware原因见上节生产环境中可以考虑改用starlette.middleware.authentication.AuthenticationMiddleware本示例是简化版。中间件顺序后添加者最外层app.add_middleware的调用顺序是理解这段代码的第二个关键点源码注释明确写道# Middleware ordering: In Starlette, the LAST added middleware is the # OUTERMOST (runs first). We need SessionMiddleware to run before # AuthMiddleware so that scope[session] is populated. So we add # AuthMiddleware first (innermost) and SessionMiddleware last (outermost). app.add_middleware(AuthMiddleware) app.add_middleware( SessionMiddleware, secret_keyos.getenv(SECRET_KEY, change-me-in-production), )Starlette 中最后添加的中间件最先执行最外层。AuthMiddleware依赖SessionMiddleware先解析 Cookie 并填充scope[session]所以SessionMiddleware必须在最外层请求先经过它解析会话再进入AuthMiddleware读取。若顺序写反scope.get(session)永远是空鉴权会全部失效。secret_key从环境变量SECRET_KEY读取用于会话 Cookie 签名注释提醒生产环境必须替换默认值。登录 / 登出路由与登录页示例用一个内联 HTML 字符串作为登录页LOGIN_PAGEmain.py并实现了三个路由GET /login渲染登录表单POST /login校验表单中的username/password与模拟用户库users_db {admin: password123}是否匹配。成功则把用户名写入request.session[username]并 302 回首页失败则渲染带红色错误提示的登录页GET /logoutrequest.session.clear()清空会话后重定向到/login。注释提醒users_db只是模拟数据生产环境应替换为真实数据库。挂载 marimo 应用最后通过marimo.create_asgi_app构建 ASGI 应用并挂载到 FastAPI 根路径marimo_app ( marimo.create_asgi_app(include_codeTrue) .with_app(path/, rootnotebook_path) .build() ) app.mount(/, marimo_app) if __name__ __main__: uvicorn.run(app, host127.0.0.1, port8000)其中notebook_path指向同目录下的 notebook.pyinclude_codeTrue表示允许在编辑模式下显示代码。整个 ASGI 应用包括其内部的 WebSocket 端点都会被外层 FastAPI 中间件链包裹因此AuthMiddleware写入的scope[user]/scope[meta]能一路透传到 marimo 的会话层。notebook 侧用 mo.app_meta().request 读取用户信息notebook.py 的核心单元格只有几行app.cell def _(mo): req mo.app_meta().request user req.user if req else None meta req.meta if req else None mo.md(f ## User info from mo.app_meta().request - **user**: {user} - **username**: {user[username] if isinstance(user, dict) else N/A} - **meta**: {meta} ) returnmo.app_meta()返回AppMeta对象定义见 marimo/_runtime/runtime.py。其request属性在 marimo/_runtime/app_meta.py 中实现从运行上下文get_context().request取值若上下文未初始化例如以脚本方式直接运行而非在应用中执行则返回None——这就是 notebook 里需要if req else None防御性判断的原因。文档约定request上通常包含headers、cookies、query_params、path_params、user、url等字段。源码纵深scope[user] / scope[meta] 如何变成 request.user / request.metamarimo 在 ASGI 边界处把scope中的身份信息转换为一个可跨进程传递的HTTPRequest对象其定义见 marimo/_runtime/commands.pydataclass class HTTPRequest(Mapping[str, Any]): Serializable HTTP request representation. Mimics Starlette/FastAPI Request but is pickle-able and contains only a safe subset of data. Excludes session and auth to prevent exposing sensitive data. url: dict[str, Encodable] base_url: dict[str, Encodable] headers: dict[str, str] query_params: dict[str, list[str]] path_params: dict[str, Encodable] cookies: dict[str, str] meta: dict[str, Encodable] # User-defined storage user: Encodable两个细节值得注意session与auth被刻意排除源码注释写明“它们可能包含应用作者不希望暴露的信息”所以你在 notebook 里拿不到原始会话对象只有中间件显式放进scope[user]/scope[meta]的数据_user_to_dict负责归一化如果scope[user]是 Starlette 的BaseUser实例如SimpleUser会被转换为{username, is_authenticated, display_name}字典因为原始对象会破坏 msgspec 序列化如果本身是字典如本示例写法则原样通过。_meta_to_dict则只强制meta是字典内部值若不可序列化会在 IPC 编码时报错而非被静默转换。由此可以推断出对scope[user]的取值建议直接用可 JSON 化的字典如示例最为稳妥键is_authenticated和username会被 marimo 内部组件如代理中间件的认证检查识别。运行示例安装 uvPep 723 内联脚本的运行依赖在examples/frameworks/fastapi-auth/目录下执行uv run --no-project main.pyuv 会自动根据文件头部的# /// script块创建临时环境并安装 fastapi、marimo、starlette、uvicorn、itsdangerous、python-multipart打开http://localhost:8000/使用admin/password123登录登录成功后notebook 单元格会通过mo.app_meta().request展示认证用户名admin与 meta 数据{role: admin}访问未登录状态下的根路径会被 302 重定向到登录页WebSocket 连接则会被以4003关闭。上生产前的注意事项结合源码与示例注释部署前建议至少处理以下事项SECRET_KEY必须通过环境变量注入强随机值否则会话 Cookie 签名可被伪造将users_db字典替换为真实用户存储密码应使用哈希如bcrypt/argon2而非明文比较若应用更复杂可评估starlette.middleware.authentication.AuthenticationMiddleware替代手写中间件——marimo 侧的CustomAuthenticationMiddlewaremarimo/_server/api/auth.py已能兼容开发者预置的scope[user]但本示例的纯 ASGI 写法对 WebSocket 的控制最直接本示例的scope[user]/scope[meta]契约只适用于 ASGI 部署路径mo.app_meta().request依赖运行上下文以脚本方式python notebook.py直接运行时request为None代码需自行兜底。这套“FastAPI 负责认证、纯 ASGI 中间件负责把身份写入scope、notebook 通过mo.app_meta().request消费身份”的模式是 marimo 官方推荐的 FastAPI 集成鉴权做法可直接作为你部署带登录的 marimo 数据应用的起点。【免费下载链接】marimoA reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. All in a modern, AI-native editor.项目地址: https://gitcode.com/GitHub_Trending/ma/marimo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考