修复中控内嵌:跨站 Cookie 被拦时用 embed_sso 维持登录。

SSO 写入 sessionStorage,fetch 自动带 X-Hub-Embed-SSO,避免期权链 API 401。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-01 11:56:51 +08:00
parent e7a4934068
commit 193c02eda8
8 changed files with 131 additions and 2 deletions
+22 -1
View File
@@ -240,6 +240,16 @@ except Exception:
_SESSION_DAYS = 7 _SESSION_DAYS = 7
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=_SESSION_DAYS) app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=_SESSION_DAYS)
app.config["SESSION_REFRESH_EACH_REQUEST"] = True app.config["SESSION_REFRESH_EACH_REQUEST"] = True
# 中控跨站 iframe 嵌入:默认 SameSite=None;Secure,否则仅首屏有会话、后续 API 全 401
_samesite = (os.getenv("SESSION_COOKIE_SAMESITE") or "None").strip() or "None"
app.config["SESSION_COOKIE_SAMESITE"] = _samesite
_secure_raw = (os.getenv("SESSION_COOKIE_SECURE") or "").strip().lower()
if _secure_raw in ("1", "true", "yes", "on"):
app.config["SESSION_COOKIE_SECURE"] = True
elif _secure_raw in ("0", "false", "no", "off"):
app.config["SESSION_COOKIE_SECURE"] = False
elif _samesite.lower() == "none":
app.config["SESSION_COOKIE_SECURE"] = True
from lib.instance.instance_embed_lib import attach_embed_templates, redirect_to_embed_shell_if_enabled from lib.instance.instance_embed_lib import attach_embed_templates, redirect_to_embed_shell_if_enabled
from lib.instance.instance_static_lib import install_common_static from lib.instance.instance_static_lib import install_common_static
@@ -5157,6 +5167,17 @@ def login_required(f):
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
if AUTH_DISABLED or bool(session.get("logged_in")): if AUTH_DISABLED or bool(session.get("logged_in")):
return f(*args, **kwargs) return f(*args, **kwargs)
# 中控 iframe:Cookie 可能被拦,改认 embed_sso
try:
from lib.hub.hub_bridge_auth import hub_embed_sso_ok
if hub_embed_sso_ok(request):
session["logged_in"] = True
session.permanent = True
session.modified = True
return f(*args, **kwargs)
except Exception:
pass
# API 必须返回 JSON,否则前端 r.json() 会报 Unexpected token... # API 必须返回 JSON,否则前端 r.json() 会报 Unexpected token...
path = request.path or "" path = request.path or ""
wants_json = ( wants_json = (
@@ -5165,7 +5186,7 @@ def login_required(f):
or "application/json" in (request.headers.get("Accept") or "") or "application/json" in (request.headers.get("Accept") or "")
) )
if wants_json: if wants_json:
return jsonify({"ok": False, "msg": "未登录或会话过期,请重新登录"}), 401 return jsonify({"ok": False, "msg": "未登录或会话过期,请从中控重新打开工作台"}), 401
return redirect("/login") return redirect("/login")
return decorated return decorated
+63
View File
@@ -0,0 +1,63 @@
/**
* 中控 iframe 嵌入鉴权:跨站时第三方 Cookie 常被拦截,改用 embed_sso 头维持登录.
* 须在其它业务脚本之前加载.
*/
(function () {
"use strict";
var KEY = "hub_embed_sso";
try {
var q = new URLSearchParams(location.search || "");
var fromUrl = (q.get("embed_sso") || "").trim();
if (fromUrl) {
sessionStorage.setItem(KEY, fromUrl);
q.delete("embed_sso");
var next = location.pathname;
var rest = q.toString();
if (rest) next += "?" + rest;
next += location.hash || "";
history.replaceState(null, "", next);
}
} catch (_) {}
function currentToken() {
try {
return (sessionStorage.getItem(KEY) || "").trim();
} catch (_) {
return "";
}
}
function withAuthHeaders(init) {
var tok = currentToken();
if (!tok) return init;
init = init ? Object.assign({}, init) : {};
var headers;
try {
headers = new Headers(init.headers || {});
} catch (_) {
headers = new Headers();
}
if (!headers.has("X-Hub-Embed-SSO")) {
headers.set("X-Hub-Embed-SSO", tok);
}
init.headers = headers;
if (!init.credentials) init.credentials = "same-origin";
return init;
}
if (typeof window.fetch === "function") {
var origFetch = window.fetch.bind(window);
window.fetch = function (input, init) {
return origFetch(input, withAuthHeaders(init));
};
}
window.HubEmbedAuth = {
token: currentToken,
clear: function () {
try {
sessionStorage.removeItem(KEY);
} catch (_) {}
},
};
})();
+19
View File
@@ -83,6 +83,25 @@ def build_embed_open_url(
return f"{root}/hub_sso?{q}" return f"{root}/hub_sso?{q}"
def extract_embed_sso(request) -> str:
"""中控 iframe 内用的短期会话令牌(Cookie 被拦时的后备)."""
raw = (request.headers.get("X-Hub-Embed-SSO") or "").strip()
if raw:
return raw
return (request.args.get("embed_sso") or "").strip()
def hub_embed_sso_ok(request) -> bool:
tok = extract_embed_sso(request)
return bool(tok and verify_sso_token(tok))
def make_embed_session_token(bridge_token: str | None = None, *, ttl_sec: int = 604800) -> str:
"""嵌入会话令牌(默认 7 天),算法与 SSO 相同."""
secret = (bridge_token if bridge_token is not None else hub_bridge_token()).strip()
return make_sso_token(secret, ttl_sec=ttl_sec)
def frame_ancestors_header_value() -> Optional[str]: def frame_ancestors_header_value() -> Optional[str]:
""" """
HUB_EMBED_FRAME_ANCESTORS: 空格分隔 Origin,如 https://hub.local http://192.168.1.10:5100 HUB_EMBED_FRAME_ANCESTORS: 空格分隔 Origin,如 https://hub.local http://192.168.1.10:5100
+8 -1
View File
@@ -4,10 +4,13 @@ from __future__ import annotations
from typing import Any from typing import Any
from flask import Flask, jsonify, redirect, request, session from flask import Flask, jsonify, redirect, request, session
from urllib.parse import quote
from lib.hub.hub_bridge_auth import ( from lib.hub.hub_bridge_auth import (
frame_ancestors_header_value, frame_ancestors_header_value,
hub_bridge_token,
hub_bridge_token_ok, hub_bridge_token_ok,
make_embed_session_token,
verify_sso_token, verify_sso_token,
) )
@@ -123,7 +126,11 @@ def register_hub_bridge_routes(app: Flask, app_module: Any) -> None:
) )
session["logged_in"] = True session["logged_in"] = True
session.permanent = True session.permanent = True
return redirect(next_path) session.modified = True
# iframe 跨站时常丢 Cookie:附带可放入 sessionStorage 的 embed_sso
embed_tok = make_embed_session_token(hub_bridge_token(), ttl_sec=604800)
joiner = "&" if "?" in next_path else "?"
return redirect(f"{next_path}{joiner}embed_sso={quote(embed_tok, safe='')}")
@app.after_request @app.after_request
def _hub_frame_ancestors(resp): def _hub_frame_ancestors(resp):
@@ -44,6 +44,17 @@ def _api_login_required(hub_token_write_allowed: bool = False):
# crypto_okx 可无 lib.hub;鉴权与中控 request_allowed 对齐 # crypto_okx 可无 lib.hub;鉴权与中控 request_allowed 对齐
allowed = bool(auth_disabled or logged_in) allowed = bool(auth_disabled or logged_in)
if not allowed:
try:
from lib.hub.hub_bridge_auth import hub_embed_sso_ok
if hub_embed_sso_ok(request):
session["logged_in"] = True
session.permanent = True
session.modified = True
allowed = True
except Exception:
pass
if not allowed and hub_hdr and bridge and hub_hdr == bridge: if not allowed and hub_hdr and bridge and hub_hdr == bridge:
allowed = True allowed = True
if not allowed: if not allowed:
+1
View File
@@ -28,6 +28,7 @@ _COMMON_STATIC_ASSETS: dict[str, str] = {
"journal_form_save.js": "application/javascript; charset=utf-8", "journal_form_save.js": "application/javascript; charset=utf-8",
"instance_page.css": "text/css; charset=utf-8", "instance_page.css": "text/css; charset=utf-8",
"instance_embed.js": "application/javascript; charset=utf-8", "instance_embed.js": "application/javascript; charset=utf-8",
"hub_embed_auth.js": "application/javascript; charset=utf-8",
"instance_mobile_nav.js": "application/javascript; charset=utf-8", "instance_mobile_nav.js": "application/javascript; charset=utf-8",
"instance_stats.js": "application/javascript; charset=utf-8", "instance_stats.js": "application/javascript; charset=utf-8",
"instance_live.js": "application/javascript; charset=utf-8", "instance_live.js": "application/javascript; charset=utf-8",
+1
View File
@@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="/static/hub_embed_auth.js?v=1"></script>
<script src="/static/instance_theme.js?v=51"></script> <script src="/static/instance_theme.js?v=51"></script>
<script src="/static/autofill_guard.js?v=1"></script> <script src="/static/autofill_guard.js?v=1"></script>
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
+6
View File
@@ -27,6 +27,12 @@ class HubBridgeAuthTests(unittest.TestCase):
self.assertIn("token=", url) self.assertIn("token=", url)
self.assertIn("next=%2Fembed", url) self.assertIn("next=%2Fembed", url)
def test_embed_session_token(self):
from lib.hub.hub_bridge_auth import make_embed_session_token
tok = make_embed_session_token("secret-token", ttl_sec=120)
self.assertTrue(verify_sso_token(tok, "secret-token"))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()