为Jupyter添加Flask前置认证层以实现远程访问的技术咨询
需求与现有配置
我希望在Jupyter前端添加Flask认证层,实现从任意地点安全访问Jupyter。Flask与Jupyter部署在同一服务器上,用户通过Flask认证后,请求需转发至Jupyter服务。以下是我当前的各项配置,寻求完善方案以确保认证后请求能正常转发至Jupyter。
Jupyter Server配置
c.ServerApp.ip = '127.0.0.1' c.ServerApp.port = 8888 c.ServerApp.open_browser = False c.ServerApp.token = '' # 禁用token认证 c.ServerApp.password = '' # 禁用密码认证 c.ServerApp.disable_check_xsrf = True # 禁用XSRF检查 c.ServerApp.trust_xheaders = True # 允许反向代理头 c.ServerApp.allow_remote_access = True c.ServerApp.allow_origin = '*'
Nginx配置
server { listen 443 ssl; server_name jupyter.mywebsite.com; ssl_certificate /etc/letsencrypt/live/jupyter.mywebsite.com/fullchain.pem; # Certbot ssl_certificate_key /etc/letsencrypt/live/jupyter.mywebsite.com/privkey.pem; # Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # Certbot SSL选项 ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; location / { proxy_pass http://127.0.0.1:7000/; # Flask服务 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Jupyter WebSocket支持 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } } server { listen 80; server_name jupyter.mywebsite.com mywebsite.com www.mywebsite.com; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name mywebsite.com www.mywebsite.com; ssl_certificate /etc/letsencrypt/live/mywebsite.com/fullchain.pem; # Certbot ssl_certificate_key /etc/letsencrypt/live/mywebsite.com/privkey.pem; # Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # Certbot SSL选项 ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; location / { proxy_pass http://127.0.0.1:7000; # Flask应用 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # WebSocket支持 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } }
Flask配置代码
# 代理Jupyter请求 @app.after_request def adjust_csp_for_jupyter(response): if "/jupyter/" in request.path: response.headers["Content-Security-Policy"] = ( "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net 'unsafe-eval'; " "style-src 'self' https://cdnjs.cloudflare.com; img-src 'self' data:;" ) return response JUPYTER_BASE_URL = "http://127.0.0.1:8888" # 内部Jupyter地址 @app.route('/jupyter/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE']) @login_required def proxy_to_jupyter(path): jupyter_url = f"{JUPYTER_BASE_URL}/{path}" try: response = requests.request( method=request.method, url=jupyter_url, headers={key: value for key, value in request.headers.items() if key.lower() not in ['host', 'cookie']}, data=request.get_data(), cookies=request.cookies, allow_redirects=False, # 手动处理重定向 ) excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] headers = [(name, value) for name, value in response.headers.items() if name.lower() not in excluded_headers] return Response(response.content, response.status_code, headers) except requests.RequestException as e: abort(502, f"连接Jupyter失败: {str(e)}") @app.route('/api/kernels/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE']) @login_required def proxy_websocket_to_jupyter(path): # WebSocket代理代码(使用flask-sock或类似库) pass
完善方案指导
1. 实现WebSocket代理功能
Jupyter依赖WebSocket与内核通信,当前Flask中的WebSocket路由为空,需用flask-sock库实现代理:
- 安装依赖:
pip install flask-sock - 修改Flask代码:
from flask_sock import Sock # 初始化Sock sock = Sock(app) @sock.route('/api/kernels/<path:path>') @login_required def proxy_websocket(path): import websocket try: # 连接到Jupyter的WebSocket服务 ws = websocket.create_connection(f"{JUPYTER_BASE_URL.replace('http', 'ws')}/{path}") # 转发客户端与Jupyter之间的消息 while True: # 接收客户端消息 client_msg = sock.receive() if client_msg is None: break ws.send(client_msg) # 接收Jupyter返回的消息 jupyter_msg = ws.recv() sock.send(jupyter_msg) except Exception as e: abort(502, f"WebSocket连接失败: {str(e)}") finally: if 'ws' in locals(): ws.close()
2. 优化Jupyter Server安全性配置
当前部分配置存在安全风险,调整如下:
c.ServerApp.ip = '127.0.0.1' c.ServerApp.port = 8888 c.ServerApp.open_browser = False c.ServerApp.token = '' c.ServerApp.password = '' # 不要禁用XSRF检查,而是让Jupyter信任代理转发的头 c.ServerApp.disable_check_xsrf = False c.ServerApp.trust_xheaders = True # 限制允许的来源为你的实际域名,而非通配符 c.ServerApp.allow_origin = 'https://jupyter.mywebsite.com https://mywebsite.com' # 仅允许来自Flask的请求(127.0.0.1) c.ServerApp.allow_remote_access = False c.ServerApp.allow_ip = ['127.0.0.1']
3. 调整Flask代理的头与重定向处理
- Host头转发:将Host设置为Jupyter的内部地址,避免域名不匹配
- Cookie处理:过滤掉Flask的会话Cookie,只转发Jupyter相关的Cookie
- 重定向处理:手动处理Jupyter的重定向,将路径替换为Flask的代理前缀
修改proxy_to_jupyter函数:
@app.route('/jupyter/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE']) @login_required def proxy_to_jupyter(path): jupyter_url = f"{JUPYTER_BASE_URL}/{path}" # 构建转发头:保留原请求头,替换Host为Jupyter的内部地址 forward_headers = { key: value for key, value in request.headers.items() if key.lower() not in ['host', 'cookie'] } forward_headers['Host'] = '127.0.0.1:8888' # 过滤Cookie:只保留Jupyter相关的(如果有的话) jupyter_cookies = {} for name, value in request.cookies.items(): if name.startswith('_jupyter_'): jupyter_cookies[name] = value try: response = requests.request( method=request.method, url=jupyter_url, headers=forward_headers, data=request.get_data(), cookies=jupyter_cookies, allow_redirects=False, ) # 处理重定向:将Jupyter的重定向路径替换为Flask的代理前缀 if response.status_code in [301, 302, 303, 307, 308]: redirect_url = response.headers['Location'] if redirect_url.startswith(JUPYTER_BASE_URL): redirect_url = redirect_url.replace(JUPYTER_BASE_URL, '/jupyter') response.headers['Location'] = redirect_url excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] headers = [(name, value) for name, value in response.headers.items() if name.lower() not in excluded_headers] return Response(response.content, response.status_code, headers) except requests.RequestException as e: abort(502, f"连接Jupyter失败: {str(e)}")
4. 优化Nginx配置
- 合并重复的配置块,减少冗余
- 确保WebSocket代理的
Connection头设置正确(使用$connection_upgrade变量)
修改Nginx配置:
# 在http块中添加以下变量定义 map $http_upgrade $connection_upgrade { default upgrade; '' close; } # 然后在所有location块中修改WebSocket设置: proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s;
5. 完善Content-Security-Policy(CSP)头
Jupyter需要更多的资源权限,调整CSP规则以兼容Jupyter的功能:
@app.after_request def adjust_csp_for_jupyter(response): if "/jupyter/" in request.path: response.headers["Content-Security-Policy"] = ( "default-src 'self'; " "script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.jsdelivr.net; " "style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; " "img-src 'self' data: blob:; " "connect-src 'self' ws://127.0.0.1:8888 wss://jupyter.mywebsite.com; " "frame-src 'self'; " "font-src 'self' https://cdnjs.cloudflare.com;" ) return response
6. 强化Flask认证的安全性
- 确保Flask的会话Cookie启用
secure、HttpOnly和SameSite属性:
app.config.update( SESSION_COOKIE_SECURE=True, SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE='Lax' )
- 仅使用HTTPS传输认证会话,避免明文泄露风险
内容的提问来源于stack exchange,提问作者jcruzer
相关产品推荐
相关产品推荐

