This commit is contained in:
dekun
2026-05-30 16:01:35 +08:00
parent 979054546c
commit cdbe087202
6 changed files with 235 additions and 20 deletions
+87
View File
@@ -0,0 +1,87 @@
"""iframe 嵌入登录(LocalNav 代签 + /embed-auth 写 SameSite=None Cookie)。"""
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import os
import time
from secrets import compare_digest
EMBED_BOOTSTRAP_TTL_SEC = int(os.getenv("NAV_EMBED_BOOTSTRAP_TTL_SEC", "120"))
def _secret(explicit: str | None = None) -> bytes:
raw = (explicit or os.getenv("NAV_SESSION_SECRET") or "").strip()
if not raw:
raw = "gate-scout-nav-embed-insecure"
return raw.encode("utf-8")
def _b64url_encode(data: bytes) -> str:
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
def _b64url_decode(text: str) -> bytes:
pad = "=" * (-len(text) % 4)
return base64.urlsafe_b64decode(text + pad)
def create_embed_bootstrap_token(username: str, *, secret: str | None = None) -> str:
"""短效 token,供 /embed-auth 在 iframe 内写入 session。"""
payload = {
"kind": "embed",
"exp": int(time.time()) + max(30, EMBED_BOOTSTRAP_TTL_SEC),
"u": (username or "admin").strip(),
}
body = _b64url_encode(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
sig = hmac.new(_secret(secret), body.encode("ascii"), hashlib.sha256).hexdigest()
return f"{body}.{sig}"
def validate_embed_bootstrap_token(token: str | None, *, secret: str | None = None) -> tuple[bool, str]:
raw = (token or "").strip()
if not raw or "." not in raw:
return False, ""
body, sig = raw.rsplit(".", 1)
try:
expect = hmac.new(_secret(secret), body.encode("ascii"), hashlib.sha256).hexdigest()
if not compare_digest(expect, sig):
return False, ""
payload = json.loads(_b64url_decode(body))
except Exception:
return False, ""
if not isinstance(payload, dict) or payload.get("kind") != "embed":
return False, ""
if int(payload.get("exp") or 0) <= int(time.time()):
return False, ""
return True, str(payload.get("u") or "admin").strip()
def safe_next_path(raw: str | None) -> str:
p = (raw or "/dashboard").strip() or "/dashboard"
if not p.startswith("/") or p.startswith("//") or "://" in p:
return "/dashboard"
return p
def request_is_https(request) -> bool:
proto = (
(request.headers.get("x-forwarded-proto") or getattr(request.url, "scheme", "http") or "http")
.split(",")[0]
.strip()
.lower()
)
return proto == "https"
def nav_embed_session_active() -> bool:
raw = (os.getenv("NAV_EMBED_SESSION") or "auto").strip().lower()
if raw in ("1", "true", "yes", "on"):
return True
if raw in ("0", "false", "no", "off"):
return False
origins = (os.getenv("NAV_EMBED_ORIGINS") or "").strip()
return bool(origins and origins != "*")