ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JupyterHub URL 强制登录实战:用外部应用与 ForcedLoginAuthenticator 隐藏 Hub 登录流程

JupyterHub URL 强制登录实战:用外部应用与 ForcedLoginAuthenticator 隐藏 Hub 登录流程 后端微服务【免费下载链接】jupyterhubMulti-user server for Jupyter notebooks项目地址https://gitcode.com/gh_mirrors/ju/jupyterhub点击查看免费下载本文基于 JupyterHub 官方文档 Logging users in via URL 与仓库中的 examples/forced-login 示例讲解如何在外部应用已经负责登录的前提下把用户无感地送进其正在运行的 JupyterHub 单用户服务器既保留 Hub 的完整认证校验又不向用户暴露 Login with... 登录页。读完后你将掌握三条实现路径OAuth 代理、REMOTE_USER 认证代理、一次性令牌交换并能结合源码理解每次跳转背后的 handler 与配置项。一、为什么旧的?tokenURL 方案在 JupyterHub 5 中默认失效过去常见的做法是以 API only mode 运行 JupyterHub为每个用户签发 token然后把用户重定向到形如/users/name/?tokenabc123的 URL 完成登录。这个做法在 JupyterHub 5 中默认被禁用了。原因在于这类 URL 携带的是用户自己可以获取和创建的凭证用户可以把它分享出去导致其他用户冒用该链接以他身份登录形成用户间inter-user攻击面。源码中可以直接验证这一默认行为HubAuth.allow_token_in_url 的定义为allow_token_in_url Bool( _bool_env(JUPYTERHUB_ALLOW_TOKEN_IN_URL, defaultFalse), helpAllow requests to pages with ?token... in the URL ... .. versionadded:: 4.1 .. versionchanged:: 5.0 default changed to False , ).tag(configTrue)从源码注释看该参数在 4.1 引入、5.0 起默认改为False唯一例外是 websocket 请求浏览器 WebSocket 实现推荐用 URL 传 token这一豁免逻辑可在 services/auth.py 中看到。由此引出本文要解决的问题作为嵌入 JupyterHub 的应用开发者如何让链接把用户送进自己的运行服务器而不触发 Hub 的登录提示同时又不重新开放用户可分享 token URL的漏洞二、问题陈述外部应用需要完成的四步按文档的问题陈述Problem statement外部应用希望做到认证用户可选创建 JupyterHub 用户启动 JupyterHub 服务器把用户重定向到正在运行的服务器全程不出现 JupyterHub 的登录提示或加载页且用户事先没有任何 JupyterHub 凭证。其中第 1 步完全由外部应用负责不是 JupyterHub 的职责第 2、3 步通过 JupyterHub REST API 完成服务需要以下 scopesadmin:users # creating users servers # start/stop servers真正的难点在第 4 步带着凭证把用户送进运行中的服务器且不留任何可见的登录交互。下面的三个方案由最推荐到兜底依次展开。三、理想方案OAuth或 REMOTE_USER 认证代理/hub/user-redirect3.1 两个关键机制最理想的设置是让外部服务自己充当 OAuth Provider某些场景下用 Shibboleth / REMOTE_USER 之类的代理认证更合适。需要掌握的核心点只有两个指向/hub/user-redirect/some/path的链接会在完成登录、确保服务器运行等步骤之后最终把用户送达/users/theirserver/some/path。该路由由 UserRedirectHandler 实现源码注释写明 /user-redirect/path/to/foowill redirect to/user/:name/path/to/foo并在 handlers/base.py 注册为/user-redirect/(.*)?。设置Authenticator.auto_login True可以让登录流程直接开始跳过 JupyterHub 的 Login with... 提示页。auto_login在 Authenticator 中的定义auto_login Bool( False, configTrue, helpAutomatically begin the login process rather than starting with a Login with... link at /hub/login To work, .login_url() must give a URL other than the default /hub/login, such as an oauth handler or another automatic login handler, registered with .get_handlers(). .. versionadded:: 0.8 , )如果你的 OAuth Provider 支持从你的 provider 免交互登录到外部服务以上两点就足够了——并非所有 Provider 都支持。若你已确保用户服务器处于运行状态用户在体验上会感觉是被直接送进了运行中的服务器但实际发生的是一连串重定向、状态检查与 cookie 写入。3.2 完整重定向链逐段解析访问/hub/user-redirect/some/path后真实发生的流程检查用户是否已登录未登录则开始登录流程/hub/login?next/hub/user-redirect/...重定向到你的 OAuth Provider 完成用户认证重定向回/hub/oauth_callback完成登录重定向回/hub/user-redirect/...认证完成后检查用户服务器是否在运行若未运行则开始启动重定向到/hub/spawn-pending/?next...启动等待页服务器就绪后重定向到真正的用户服务器/users/username/some/path在用户服务器上检查 cookie 中的 token若不存在或无效对 Hub 发起 OAuth重定向到/hub/api/oauth2/authorize/...Hub 重定向回/users/user/oauth_callback完成 OAuth再次重定向到本次内部 OAuth 最初出发的 URL最终带着有效的 JupyterHub 凭证抵达/users/username/some/path这套重定向序列在 JupyterHub 的每次启动流程中都在发生正常情况下对用户完全透明。3.3 哪些环节会打扰用户上述流程中唯一可能让用户看到非目标页面的步骤步骤用户可能看到什么控制方式1.1Login with... 登录页设置c.Authenticator.auto_login True消除1.2你的 OAuth Provider 的登录提示由 Provider 决定JupyterHub 无法控制可能无法避免2.2spawn-pending 等待页仅当服务器尚未运行时出现可由外部服务预先启动服务器规避其余全部是对最终目的地的透明重定向。3.4 使用认证代理REMOTE_USER如果你使用 Shibboleth 这类设置REMOTE_USER请求头的认证代理可以选用基于请求头自动登录用户的 Authenticator社区有RemoteUserAuthenticator之类的实现文档中引用了 jhub_remote_user_authenticator 项目。流程与 OAuth 路径相同区别是第 1.1 步不重定向到 provider而是立即完成登录。安全警告文档原文强调如果你依赖认证代理必须极其确信请求只来自该认证代理绝不能接受来自其他来源、自行设置REMOTE_USER头的请求。四、定制方案ForcedLoginAuthenticator 一次性令牌交换假设你既不能用 OAuth 也不能用 REMOTE_USER但仍想隐藏 JupyterHub 的实现细节——你真正需要的只是一个能把用户送到其服务器、且没有任何登录提示的 URL。仓库在 examples/forced-login 中提供了完整示例做法是实现一个auto_login True的 Authenticator根据请求中的信息如查询参数登录用户。4.1 外部应用侧签发一次性令牌示例中的外部服务external_app.py是一个 FastAPI 应用提供一个表单页index.html用户输入用户名和目标路径。点击登录时发生三件事用secrets.token_urlsafe(32)生成随机 token将SHA-256 哈希后的 token与用户名、过期时间签发后 30 秒token_lifetime 30一起存入内存字典以 303 重定向到/hub/login?login_token...next/hub/user-redirect/destination/path。对应的关键源码external_app.pyapp.post(/) async def launch(username: Annotated[str, Form()], path: Annotated[str, Form()]): Begin login 1. issue token for login 2. associate token with username 3. redirect to /hub/login?login_token... token secrets.token_urlsafe(32) hashed_token _hash(token) _tokens_to_username[hashed_token] (username, time.monotonic() token_lifetime) login_url (jupyterhub_url / hub/login).extend_query( login_tokentoken, nexturl_path_join(/hub/user-redirect, path) ) return RedirectResponse(login_url, status_codestatus.HTTP_303_SEE_OTHER)随后 Hub 会回调POST /login用 token 换用户名该端点保证令牌只能用一次、且必须在 30 秒内使用external_app.pyapp.post(/login, response_classJSONResponse) async def login(token: Annotated[str, Body(embedTrue)]): Callback to exchange a token for a username. token is consumed, can only be used once now time.monotonic() hashed_token _hash(token) if hashed_token not in _tokens_to_username: return JSONResponse(status_codestatus.HTTP_404_NOT_FOUND, content{message: invalid token}) username, expires_at _tokens_to_username.pop(hashed_token) # pop: 用后即销毁 if expires_at now: return JSONResponse(status_codestatus.HTTP_400_BAD_REQUEST, content{message: token expired}) return {name: username}4.2 JupyterHub 侧ForcedLoginAuthenticator 完整配置JupyterHub 侧的ForcedLoginAuthenticator完整实现见 examples/forced-login/jupyterhub_config.py实现了authenticate其职责是auto_login True——访问/hub/login时直接调用authenticate()而不是渲染登录页从login_tokenURL 参数取出 token携带 token 向外部应用发起 POST 请求请求换取用户名外部应用返回用户名并删除 token使其无法复用Authenticator 把返回值含name的 dict透传为认证结果。完整配置代码含本地测试用的 fake spawner 设置import json from tornado import web from tornado.httpclient import AsyncHTTPClient, HTTPClientError from traitlets import Unicode from jupyterhub.auth import Authenticator from jupyterhub.utils import url_path_join class ForcedLoginAuthenticator(Authenticator): Authenticator to force login with a token provided by an external service The external service issues tokens, which are exchanged for a username. Visiting /hub/login?login_token... logs in a user Each token can be used only once. auto_login True # begin login without prompt (token is in url) allow_all True # external login app controls this token_provider_url Unicode( configTrue, helpThe URL of the token/username provider ) async def authenticate(self, handler, data): token handler.get_argument(login_token, None) if not token: raise web.HTTPError( 400, fLogin with external provider at {self.token_provider_url} ) client AsyncHTTPClient() try: response await client.fetch( url_path_join(self.token_provider_url, /login), methodPOST, headers{Content-Type: application/json}, bodyjson.dumps({token: token}), ) except HTTPClientError as e: self.log.info( Error exchanging token for username: %s, e.response.body.decode(utf8, replace), ) if e.code 404: raise web.HTTPError( 403, fInvalid token. Login with external provider at {self.token_provider_url}, ) else: raise # pass through the response return json.loads(response.body.decode()) c get_config() # noqa # use our Authenticator c.JupyterHub.authenticator_class ForcedLoginAuthenticator # tell it where the external launch app is c.ForcedLoginAuthenticator.token_provider_url http://127.0.0.1:9000/ # local testing config (fake spawner, localhost only) c.JupyterHub.ip 127.0.0.1 c.JupyterHub.spawner_class simple关键配置项说明配置项默认值作用auto_loginFalse基类True时访问/hub/login直接触发authenticate()不展示登录页示例中硬编码为Trueallow_allFalse基类True表示允许登录哪些人完全交给外部应用裁决jupyterhub_config.py 中的注释原话token_provider_url无默认外部令牌/用户名提供方服务的 URL示例设为http://127.0.0.1:9000/注意错误分支的处理逻辑外部应用对无效 token 返回 404Authenticator 将其转成 403 提示Invalid token其他 HTTP 错误原样抛出便于排查。4.3 运行示例按 examples/forced-login/README.md 的说明# 终端一启动外部应用监听 127.0.0.1:9000 python3 external_app.py # 终端二启动 JupyterHub使用上面的 jupyterhub_config.pyfake spawner、仅监听 localhost jupyterhub然后访问http://127.0.0.1:9000输入用户名与目标路径默认/lab即完成强制登录。该 README 同时点明了安全语义用户不能直接登录 JupyterHub只能经由这个应用进入每个 token 只能用一次且必须在签发后 30 秒内使用。五、与 API only mode 组合如果你的服务本来就通过 API 启动服务器可以把整个部署跑在 API only mode 下只需额外把/hub/login也暴露出来c.JupyterHub.hub_routespec /hub/api/ c.Proxy.additional_routes {/hub/login: http://hub:8080}这样/hub/login?login_token...依然可访问用于完成令牌交换登录而 JupyterHub 的其余 UI 对用户完全不可见。六、为什么这套设计是安全的隐藏而非绕过文档在末尾专门回答了 Why does this work?这依然是 URL 中带 token 登录对吧是的但关键区别是用户无法签发这些 token。旧方案?tokenabc123直达用户服务器泄露的 token 是用户自己可以创建和持有的长期凭证本方案中login_token只能由受信任的外部应用在认证用户后即时签发且用后即焚 30 秒过期见 external_app.py 的token_lifetime 30。文档同时诚实地指出现有示例的剩余弱点该 token 链接理论上应当不可转移non-transferrable即使只能使用一次——如果 A 用户把链接转发给 BB 仍可能抢先消费。示例应用目前唯一的防线就是 token 快速过期30 秒。更严格的部署可以像 OAuth 那样引入 state cookie 等机制到那一步文档的建议是干脆直接实现 OAuth 吧。需要强调的是这套流程并没有绕过 JupyterHub 认证——某些部署曾经用绕过的方式处理登录而那才是真正危险的做法这里只是把登录交互隐藏了authenticate()依然经过 Authenticator 的完整校验路径。七、关键源码索引内容位置allow_token_in_url默认FalseJupyterHub 5的定义与版本变更记录jupyterhub/services/auth.py#L370-L391auto_login配置项0.8 引入jupyterhub/auth.py#L959-L972/hub/user-redirect/...路由与UserRedirectHandlerjupyterhub/handlers/base.py#L1998-L2081user_redirect与user_redirect_hook的应用入口jupyterhub/app.py#L1434、jupyterhub/app.py#L1781ForcedLoginAuthenticator参考实现examples/forced-login/jupyterhub_config.py外部令牌签发/交换服务examples/forced-login/external_app.py官方 How-to 文档原文docs/source/howto/forced-login.md选型建议小结能走 OAuth或 REMOTE_USER 认证代理就走标准路径配合auto_login True与/hub/user-redirect多数跳转天然透明只有在两条标准路径都不可用时才用ForcedLoginAuthenticator 一次性短时效令牌这种定制方案——它牺牲了少量安全性token 仍出现在 URL 中换取用户零登录交互的体验且必须自行收紧令牌的转移风险。赞分享后端微服务【免费下载链接】jupyterhubMulti-user server for Jupyter notebooks项目地址https://gitcode.com/gh_mirrors/ju/jupyterhub点击查看免费下载相关推荐Wasp 0.11.8 集成 GitHub 登录从 OAuth 应用到完整登录流程的实战指南Wasp 0.11.8 集成 GitHub 登录从 OAuth 应用到完整登录流程的实战指南 Wasp 框架内置了对 GitHub 社交登录GitHub AWeb框架后端前端CLI开发工具JupyterHub GitHub OAuth 登录配置实战使用 LocalGitHubOAuthenticator 完成生产级部署JupyterHub GitHub OAuth 登录配置实战使用 LocalGitHubOAuthenticator 完成生产级部署 本篇基于仓库文档 doc后端微服务Feathers 登录实战用户注册、JWT 签发与 GitHub OAuth 登录全流程解析Feathers 登录实战用户注册、JWT 签发与 GitHub OAuth 登录全流程解析 本篇技术指南基于 Feathers 官方聊天应用教程的登录章节后端Web框架上一篇高分辨率二值图像分割的革命BiRefNet技术深度解析下一篇Python开发者必看bilibili-api-python安装失败的3个终极解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表