feat(auth): 独立登录会话开关(设置-外观)—— 同浏览器多账号互不覆盖
复用 iframe 嵌入的按标签页隔离机制:新增 ?isolate=1 标志,authStorage() 从「仅嵌入」放宽为 isEmbedActive() || isIsolatedSession(),独立态走 sessionStorage 故本标签页登录态不被其它标签页覆盖。设置→外观→「登录会话」 加 Switch,开启时先 migrateAuthToSession() 把 localStorage 受保护键快照搬进 sessionStorage 再翻标志,无感不登出;AuthSyncWatcher 跨标签监听对隔离态实时 忽略,运行时开关即刻生效。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a9f01bec79
commit
a85d2480b4
5 files changed
+146
-15
No files matched your search
@@ -17,15 +17,17 @@
|
||||
* sessionStorage、正常态恒为 localStorage;嵌入标志一进程内不翻转,故一致)。
|
||||
*/
|
||||
|
||||
import { isEmbedActive } from "@/core/embed/embed-session";
|
||||
import { isEmbedActive, isIsolatedSession } from "@/core/embed/embed-session";
|
||||
|
||||
/**
|
||||
* 返回当前应使用的鉴权存储:嵌入态 → sessionStorage(按标签页隔离);否则 → localStorage。
|
||||
* 非浏览器环境返回 null(调用方各自有 SSR 守卫)。
|
||||
* 返回当前应使用的鉴权存储:嵌入态(iframe)**或**独立会话态(`?isolate=1`)→ sessionStorage
|
||||
* (按标签页隔离);否则 → localStorage(跨标签共享、持久)。非浏览器环境返回 null(调用方各自
|
||||
* 有 SSR 守卫)。两种隔离态等价于「本标签页鉴权独立、绝不污染其它标签页登录态」,区别仅在于嵌入态
|
||||
* 还会去 chrome / 强制主题,独立会话态只切换存储。
|
||||
*/
|
||||
export function authStorage(): Storage | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return isEmbedActive() ? window.sessionStorage : window.localStorage;
|
||||
return isEmbedActive() || isIsolatedSession() ? window.sessionStorage : window.localStorage;
|
||||
}
|
||||
|
||||
/** 读受保护键(嵌入态 sessionStorage / 正常态 localStorage)。 */
|
||||
@@ -54,3 +56,39 @@ export function authStorageRemove(key: string): void {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 受 `authStorage()` 收口管理的鉴权 / 身份键:四个精确键 + `login.` 前缀(外部登录参数族
|
||||
* ticket/userId/env/maxkeyOnlineToken/userDetailToken)。用于「开启独立会话」时把当前登录态从
|
||||
* localStorage 快照搬进 sessionStorage,避免切换存储后被登出。
|
||||
*/
|
||||
const PROTECTED_AUTH_EXACT_KEYS = ["deerflow.auth", "userInfo", "userId", "tools_token"];
|
||||
const PROTECTED_AUTH_PREFIX = "login.";
|
||||
|
||||
function isProtectedAuthKey(key: string): boolean {
|
||||
return PROTECTED_AUTH_EXACT_KEYS.includes(key) || key.startsWith(PROTECTED_AUTH_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 localStorage 里的鉴权 / 身份键**快照复制**进 sessionStorage(覆盖式)。
|
||||
*
|
||||
* 「外观」设置开启独立会话时调用:此后 `authStorage()` 改读 sessionStorage,若不先搬一份当前账号
|
||||
* 过去就会瞬间「未登录」。每次开启都重新快照(覆盖旧值),确保拿到的是当前 localStorage 的最新账号。
|
||||
* 仅搬受保护键,不动 sessionStorage 里既有的本标签页专属态(embed.session / 任务上下文等)。
|
||||
*/
|
||||
export function migrateAuthToSession(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
const ls = window.localStorage;
|
||||
const ss = window.sessionStorage;
|
||||
for (let i = 0; i < ls.length; i++) {
|
||||
const key = ls.key(i);
|
||||
if (key && isProtectedAuthKey(key)) {
|
||||
const value = ls.getItem(key);
|
||||
if (value !== null) ss.setItem(key, value);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { useEffect, useRef } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { isEmbedActive } from "@/core/embed/embed-session";
|
||||
import { isEmbedActive, isIsolatedSession } from "@/core/embed/embed-session";
|
||||
|
||||
const AUTH_STORAGE_KEY = "deerflow.auth";
|
||||
const SETTLE_DELAY_MS = 1200;
|
||||
@@ -22,9 +22,9 @@ export function AuthSyncWatcher() {
|
||||
const lastUserIdRef = useRef<string | null>(readUserId());
|
||||
|
||||
useEffect(() => {
|
||||
// 嵌入(iframe)态:鉴权走按标签页的 sessionStorage,本就不与其它标签页共享,且不应被正常
|
||||
// 标签页的 localStorage 账号变更打扰 —— 直接不挂跨标签监听。
|
||||
if (isEmbedActive()) return;
|
||||
// 嵌入(iframe)态 / 独立会话态(?isolate=1):鉴权走按标签页的 sessionStorage,本就不与其它标签页
|
||||
// 共享,且不应被其它标签页的 localStorage 账号变更打扰 —— 直接不挂跨标签监听。
|
||||
if (isEmbedActive() || isIsolatedSession()) return;
|
||||
|
||||
let settleTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
@@ -52,6 +52,11 @@ export function AuthSyncWatcher() {
|
||||
}
|
||||
|
||||
function handleStorage(event: StorageEvent) {
|
||||
// 运行时若已切到独立会话(「外观」开关在挂载后才开启):鉴权改读本标签页 sessionStorage,
|
||||
// 不应再被其它标签页的 localStorage 账号变更打扰 —— 即时忽略,无需刷新页面。
|
||||
if (isEmbedActive() || isIsolatedSession()) {
|
||||
return;
|
||||
}
|
||||
// Ignore unrelated keys and localStorage.clear(). Backend session loss is
|
||||
// handled centrally through 401 responses.
|
||||
if (event.key !== AUTH_STORAGE_KEY) {
|
||||
|
||||
Reference in new issue
Block a user