feat: add nginx, proxy to jupyter after auth

This commit is contained in:
tao.chen
2026-07-27 19:58:17 +08:00
parent 1c03a32298
commit ab731bdc3b
4 changed files with 214 additions and 112 deletions
+146
View File
@@ -0,0 +1,146 @@
# coding=utf-8
"""
@Time :2026/7/27
@Author :tao.chen
"""
import os
import re
import httpx
from fastapi import FastAPI, Request, Response, HTTPException, Depends, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from typing import Optional
from loguru import logger
app = FastAPI(title="Jupyter Auth & Router Backend")
RUNTIME_BASE_URL = os.getenv("RUNTIME_BASE_URL", "http://127.0.0.1:8001")
security = HTTPBearer(auto_error=False)
# ------------------------------------------------------------------
# 1. Runtime 交互 Client
# ------------------------------------------------------------------
class RuntimeClient:
"""与 Runtime 进程管理器服务交互"""
@staticmethod
async def get_workspace(workspace_id: str) -> Optional[dict]:
"""按需查询单个 workspace 进程"""
async with httpx.AsyncClient(base_url=RUNTIME_BASE_URL) as client:
try:
resp = await client.post(
"/api/v1/jupyter",
json={"action": "get", "workspace_id": workspace_id},
timeout=3.0,
)
if resp.status_code == 200:
return resp.json()
return None
except httpx.RequestError:
return None
@staticmethod
async def start_workspace(workspace_id: str) -> dict:
"""进程未运行时主动触发启动"""
async with httpx.AsyncClient(base_url=RUNTIME_BASE_URL) as client:
resp = await client.post(
"/api/v1/jupyter",
json={"action": "start", "workspace_id": workspace_id},
timeout=10.0,
)
if resp.status_code == 200:
return resp.json()
raise HTTPException(
status_code=500, detail="Failed to start Jupyter instance"
)
# ------------------------------------------------------------------
# 2. 数据库与权限模拟 (请根据实际 MySQL ORM 修改)
# ------------------------------------------------------------------
async def check_notebook_is_locked(workspace_id: str, notebook_path: str) -> bool:
"""
查数据库:判断特定 Notebook 文件是否被锁定
:param workspace_id: 工作区 ID
:param notebook_path: 相对路径,如 "test.ipynb""folder/demo.ipynb"
"""
# 模拟锁定数据库:假定 test_locked.ipynb 被锁定
locked_notebooks = {
("test1234", "test_locked.ipynb"): True,
}
return locked_notebooks.get((workspace_id, notebook_path), False)
def verify_jwt_token(token: str) -> str:
"""校验 JWT 令牌"""
if token == "invalid-token":
raise HTTPException(status_code=401, detail="Invalid Authentication Token")
return "user_001"
def extract_notebook_path(uri: str, workspace_id: str) -> Optional[str]:
"""
从原始请求 URI 中提取请求的 .ipynb 文件相对路径
例如: /jupyter/test1234/notebooks/folder/test.ipynb -> folder/test.ipynb
"""
pattern = rf"^/jupyter/{re.escape(workspace_id)}/notebooks/(.+\.ipynb)"
match = re.match(pattern, uri)
if match:
return match.group(1)
return None
# ------------------------------------------------------------------
# 3. 核心 Auth 接口 (针对 Nginx auth_request)
# ------------------------------------------------------------------
@app.get("/api/v1/auth/jupyter")
async def verify_jupyter_access(
request: Request,
response: Response,
auth: Optional[HTTPAuthorizationCredentials] = Depends(security),
):
# 获取 Nginx 传入的元数据
workspace_id = request.headers.get("X-Original-Workspace-Id")
original_uri = request.headers.get("X-Original-URI", "")
cookie_token = request.cookies.get("access_token")
bearer_token = auth.credentials if auth else None
token = bearer_token or cookie_token
# if not token:
# raise HTTPException(status_code=401, detail="Missing Authentication Token")
if not workspace_id:
raise HTTPException(status_code=400, detail="Missing Workspace ID")
# 基础身份认证
# current_user_id = verify_jwt_token(token)
# 精准锁校验:只有在访问 .ipynb 文件时才检查 is_locked
notebook_path = extract_notebook_path(original_uri, workspace_id)
if notebook_path:
is_locked = await check_notebook_is_locked(workspace_id, notebook_path)
if is_locked:
raise HTTPException(
status_code=403,
detail=f"Notebook '{notebook_path}' is currently locked",
)
# 获取或启动 Jupyter 子进程
ws_info = await RuntimeClient.get_workspace(workspace_id)
if not ws_info or ws_info.get("status") != "running":
ws_info = await RuntimeClient.start_workspace(workspace_id)
target_port = ws_info.get("port")
jupyter_token = ws_info.get("token")
if not target_port:
raise HTTPException(
status_code=500, detail="Jupyter instance returned no port"
)
# 通过 Response Header 返回 Upstream 地址与 Token 给 Nginx
response.headers["x-upstream-addr"] = f"http://192.168.139.3:{target_port}"
response.headers["x-jupyter-internal-token"] = jupyter_token or ""
return {"status": "ok"}