From c882a5e5655f1078431eb55654b83a6a04c5a613 Mon Sep 17 00:00:00 2001 From: dekun Date: Sat, 30 May 2026 12:13:33 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=AD=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto_monitor_okx/.env.example | 2 + hub_bridge.py | 43 +++++++++++++++++++- manual_trading_hub/.env.example | 1 + manual_trading_hub/static/app.js | 61 +++++++++++++++++++++------- manual_trading_hub/static/index.html | 2 +- manual_trading_hub/常见问题.md | 3 +- 6 files changed, 94 insertions(+), 18 deletions(-) diff --git a/crypto_monitor_okx/.env.example b/crypto_monitor_okx/.env.example index e87c75a..9d8018e 100644 --- a/crypto_monitor_okx/.env.example +++ b/crypto_monitor_okx/.env.example @@ -33,6 +33,8 @@ APP_AUTH_DISABLED=true # 允许复盘中控 iframe 内嵌本实例(与 hub 域名一致;默认已开启) # APP_ALLOW_HUB_EMBED=true # HUB_EMBED_PARENT_ORIGINS=https://hub.example.com +# HTTPS 且中控与实例不同域名时必开,否则 hub-sso 登录态在 iframe 内无法保存 +# APP_COOKIE_SECURE=true # Flask 会话密钥(必须替换为长随机字符串) FLASK_SECRET_KEY=CHANGE_TO_LONG_RANDOM_SECRET diff --git a/hub_bridge.py b/hub_bridge.py index 54458d3..9dd6351 100644 --- a/hub_bridge.py +++ b/hub_bridge.py @@ -9,7 +9,15 @@ import json import time from functools import wraps -from flask import current_app, get_flashed_messages, jsonify, redirect, request, session +from flask import ( + current_app, + flash, + get_flashed_messages, + jsonify, + redirect, + request, + session, +) from hub_auth import request_allowed from hub_sso import safe_next_path, verify_hub_sso_token @@ -109,9 +117,32 @@ def install_on_app( "views": views, } install_hub_embed_headers(app) + configure_hub_embed_session(app) register_hub_routes(app) +def configure_hub_embed_session(app): + """HTTPS 跨域 iframe 内嵌时须 SameSite=None + Secure,否则 hub-sso 写入的 session 会丢失。""" + import os + + allowed = (os.getenv("APP_ALLOW_HUB_EMBED") or "true").strip().lower() in ( + "1", + "true", + "yes", + "on", + ) + if not allowed: + return + secure = (os.getenv("APP_COOKIE_SECURE") or "").strip().lower() + if secure not in ("1", "true", "yes", "on"): + return + app.config.update( + SESSION_COOKIE_SECURE=True, + SESSION_COOKIE_SAMESITE="None", + SESSION_COOKIE_HTTPONLY=True, + ) + + def install_hub_embed_headers(app): """允许复盘中控 iframe 内嵌打开本实例(须与 hub 的 HUB_EMBED_ORIGINS 或域名一致)。""" import os @@ -286,10 +317,18 @@ def register_hub_routes(app): return redirect(safe_next_path(next_arg)) ex = str((_ctx().get("exchange") or "")).strip().lower() token = (request.args.get("token") or "").strip() - ok, next_path, _err = verify_hub_sso_token(token, ex) + ok, next_path, err = verify_hub_sso_token(token, ex) if ok: session["logged_in"] = True + session.modified = True return redirect(next_path) + hint = err or "校验失败" + flash( + f"中控 SSO 未生效({hint})。" + "请确认中控与实例 .env 中 HUB_BRIDGE_TOKEN 一致," + f"且中控设置里该账户 key 为「{ex}」。" + "直链实例地址仍需输入 APP_PASSWORD。" + ) return redirect("/login") diff --git a/manual_trading_hub/.env.example b/manual_trading_hub/.env.example index b3abe7b..8e9da6d 100644 --- a/manual_trading_hub/.env.example +++ b/manual_trading_hub/.env.example @@ -44,6 +44,7 @@ HUB_TRUST_LAN=true # 四实例允许被中控 iframe 内嵌(各 crypto_monitor_*/.env,与 hub 同步部署) # APP_ALLOW_HUB_EMBED=true # HUB_EMBED_PARENT_ORIGINS=https://hub.example.com +# HTTPS 跨子域 iframe 时四实例还须 APP_COOKIE_SECURE=true(见 crypto_monitor_*/.env.example) # 浏览器打开的实例/复盘链接(hub_settings 里 flask_url 为 127.0.0.1 时替换为对外地址) # 局域网:填内网 IP,见《局域网与反代部署说明.md》 diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 5b44af4..a5127ae 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -18,25 +18,55 @@ } let instanceFrameUrl = ""; + /** @type {{ exchangeId: string, nextPath: string, title: string } | null} */ + let instanceFrameCtx = null; + + async function fetchInstanceOpenUrl(exchangeId, nextPath) { + const next = nextPath || "/"; + const q = new URLSearchParams({ exchange_id: String(exchangeId), next }); + const r = await apiFetch("/api/instance/open-url?" + q.toString()); + const j = await r.json(); + if (!j.ok || !j.url) { + throw new Error(j.detail || "无法生成打开链接"); + } + return j.url; + } async function openInstance(exchangeId, nextPath, opts) { const options = opts || {}; const newTab = !!options.newTab; const next = nextPath || "/"; try { - const q = new URLSearchParams({ exchange_id: String(exchangeId), next }); - const r = await apiFetch("/api/instance/open-url?" + q.toString()); - const j = await r.json(); - if (!j.ok || !j.url) { - showToast(j.detail || "无法生成打开链接", true); - return; - } + const url = await fetchInstanceOpenUrl(exchangeId, next); if (newTab) { - window.open(j.url, "_blank", "noopener"); + window.open(url, "_blank", "noopener"); return; } const row = lastMonitorRows.find((x) => String(x.id) === String(exchangeId)); - openInstanceFrame(j.url, row ? row.name : exchangeId); + const title = row ? row.name : exchangeId; + instanceFrameCtx = { exchangeId: String(exchangeId), nextPath: next, title }; + openInstanceFrame(url, title); + } catch (e) { + showToast(String(e), true); + } + } + + async function refreshInstanceFrame() { + if (!instanceFrameCtx) { + if (instanceFrameUrl) { + const frame = document.getElementById("instance-frame"); + if (frame) frame.src = instanceFrameUrl; + } + return; + } + try { + const url = await fetchInstanceOpenUrl( + instanceFrameCtx.exchangeId, + instanceFrameCtx.nextPath + ); + instanceFrameUrl = url; + const frame = document.getElementById("instance-frame"); + if (frame) frame.src = url; } catch (e) { showToast(String(e), true); } @@ -63,6 +93,7 @@ const shell = document.getElementById("instance-frame-shell"); const frame = document.getElementById("instance-frame"); instanceFrameUrl = ""; + instanceFrameCtx = null; if (frame) frame.src = "about:blank"; if (shell) { shell.classList.add("hidden"); @@ -979,13 +1010,15 @@ const newTab = document.getElementById("instance-frame-newtab"); const frame = document.getElementById("instance-frame"); if (back) back.onclick = () => closeInstanceFrame(); - if (refresh && frame) { - refresh.onclick = () => { - if (instanceFrameUrl) frame.src = instanceFrameUrl; - }; - } + if (refresh) refresh.onclick = () => refreshInstanceFrame(); if (newTab) { newTab.onclick = () => { + if (instanceFrameCtx) { + openInstance(instanceFrameCtx.exchangeId, instanceFrameCtx.nextPath, { + newTab: true, + }); + return; + } if (instanceFrameUrl) window.open(instanceFrameUrl, "_blank", "noopener"); }; } diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 0253c9f..bd46314 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -120,6 +120,6 @@
- + diff --git a/manual_trading_hub/常见问题.md b/manual_trading_hub/常见问题.md index 01cc1a8..f973acf 100644 --- a/manual_trading_hub/常见问题.md +++ b/manual_trading_hub/常见问题.md @@ -226,7 +226,8 @@ HUB_PUBLIC_ORIGIN=http://192.168.8.6 1. 四实例未重启,`/hub-sso` 未加载(启动日志勿长期 `[hub_bridge] ImportError`)。 2. `HUB_BRIDGE_TOKEN` 与四实例 `.env` 不一致。 3. `hub_settings` 里该户 `key` 与实例 `install_on_app(exchange=...)` 不一致(如 `okx`、`gate_bot`)。 -4. 浏览器仍用旧书签直链首页,未从中控点「实例」(直链本来就要登录)。 +4. **HTTPS 跨域 iframe**:中控与实例不同域名时,四实例须 `APP_COOKIE_SECURE=true`(使 session Cookie 为 `SameSite=None`),否则 SSO 成功仍跳 `/login`。 +5. 浏览器仍用旧书签直链首页,未从中控点「实例」(直链本来就要登录)。 **直链**:`http://IP:端口` 或 `https://实例域名` → 使用各实例 **`APP_USERNAME` / `APP_PASSWORD`**(四所建议统一)。