修复中控内嵌:跨站 Cookie 被拦时用 embed_sso 维持登录。
SSO 写入 sessionStorage,fetch 自动带 X-Hub-Embed-SSO,避免期权链 API 401。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 (_) {}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user