统一外部 access_token 存储与轻应用 URL 拼接,修复嵌入会话鉴权隔离。
将 access_token 纳入 scoped storage 迁移,yUserId 登录优先并独立持久化 token;轻应用/按钮管理默认改用 access_token 参数名,支持 hash 路由占位符替换与自动追加。
This commit is contained in:
1 parent
8c6d08d6bf
commit
6378cd33bd
10 files changed
+210
-39
No files matched your search
@@ -1,3 +1,7 @@
|
||||
import { authStorageGet, authStorageSet } from "@/core/auth/scoped-storage";
|
||||
|
||||
export const EXTERNAL_ACCESS_TOKEN_STORAGE_KEY = "access_token";
|
||||
|
||||
export function normalizeExternalAccessToken(value: unknown): string {
|
||||
if (value == null) return "";
|
||||
const token = String(value).trim();
|
||||
@@ -5,3 +9,53 @@ export function normalizeExternalAccessToken(value: unknown): string {
|
||||
const lowered = token.toLowerCase();
|
||||
return lowered === "null" || lowered === "undefined" ? "" : token;
|
||||
}
|
||||
|
||||
function readTokenFromStoredValue(raw: string | null): string {
|
||||
const token = normalizeExternalAccessToken(raw);
|
||||
if (!token) return "";
|
||||
if (!token.startsWith("{")) return token;
|
||||
try {
|
||||
const parsed = JSON.parse(token);
|
||||
if (!parsed || typeof parsed !== "object") return token;
|
||||
const obj = parsed as Record<string, unknown>;
|
||||
return (
|
||||
normalizeExternalAccessToken(obj.access_token) ||
|
||||
normalizeExternalAccessToken(obj.accessToken) ||
|
||||
normalizeExternalAccessToken(obj.token)
|
||||
);
|
||||
} catch {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
export function readStoredExternalAccessTokenValue(): string {
|
||||
return readTokenFromStoredValue(authStorageGet(EXTERNAL_ACCESS_TOKEN_STORAGE_KEY));
|
||||
}
|
||||
|
||||
export function readExternalAccessTokenValue(userInfo: Record<string, unknown>): string {
|
||||
return normalizeExternalAccessToken(userInfo.access_token) || normalizeExternalAccessToken(userInfo.accessToken);
|
||||
}
|
||||
|
||||
export function readCurrentExternalAccessTokenValue(userInfo: Record<string, unknown>): string {
|
||||
return readStoredExternalAccessTokenValue() || readExternalAccessTokenValue(userInfo);
|
||||
}
|
||||
|
||||
export function writeExternalAccessTokenFields(
|
||||
userInfo: Record<string, unknown>,
|
||||
token: string,
|
||||
): void {
|
||||
userInfo.access_token = token;
|
||||
userInfo.accessToken = token;
|
||||
}
|
||||
|
||||
export function writeStoredExternalAccessToken(token: string): void {
|
||||
const normalized = normalizeExternalAccessToken(token);
|
||||
if (normalized) {
|
||||
authStorageSet(EXTERNAL_ACCESS_TOKEN_STORAGE_KEY, normalized);
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteExternalAccessTokenFields(userInfo: Record<string, unknown>): void {
|
||||
delete userInfo.access_token;
|
||||
delete userInfo.accessToken;
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export function authStorageRemove(key: string): void {
|
||||
* ticket/userId/env/maxkeyOnlineToken/userDetailToken)。用于「开启独立会话」时把当前登录态从
|
||||
* localStorage 快照搬进 sessionStorage,避免切换存储后被登出。
|
||||
*/
|
||||
const PROTECTED_AUTH_EXACT_KEYS = ["deerflow.auth", "userInfo", "userId", "tools_token"];
|
||||
const PROTECTED_AUTH_EXACT_KEYS = ["deerflow.auth", "userInfo", "userId", "tools_token", "access_token"];
|
||||
const PROTECTED_AUTH_PREFIX = "login.";
|
||||
|
||||
function isProtectedAuthKey(key: string): boolean {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { requestCrudService } from "@/service/request-axios-service";
|
||||
import { authStorageGet, authStorageSet } from "@/core/auth/scoped-storage";
|
||||
import { normalizeExternalAccessToken } from "@/core/auth/external-access-token";
|
||||
import {
|
||||
deleteExternalAccessTokenFields,
|
||||
normalizeExternalAccessToken,
|
||||
readStoredExternalAccessTokenValue,
|
||||
readExternalAccessTokenValue,
|
||||
writeExternalAccessTokenFields,
|
||||
writeStoredExternalAccessToken,
|
||||
} from "@/core/auth/external-access-token";
|
||||
|
||||
const CONSUMER_URL = import.meta.env.VITE_AIPI_CONSUMER_URL || "";
|
||||
|
||||
@@ -36,6 +43,38 @@ function readStoredYUserId(info: Record<string, unknown>): string {
|
||||
return value == null ? "" : String(value).trim();
|
||||
}
|
||||
|
||||
function normalizeExternalUserId(value: unknown): string {
|
||||
if (value == null) return "";
|
||||
const text = String(value).trim();
|
||||
if (!text) return "";
|
||||
const lowered = text.toLowerCase();
|
||||
return lowered === "null" || lowered === "undefined" ? "" : text;
|
||||
}
|
||||
|
||||
function maskToken(token: string): string {
|
||||
if (!token) return "";
|
||||
if (token.length <= 10) return `${token.slice(0, 2)}***(${token.length})`;
|
||||
return `${token.slice(0, 6)}...${token.slice(-4)}(${token.length})`;
|
||||
}
|
||||
|
||||
function summarizeUserInfo(info: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
keys: Object.keys(info),
|
||||
yUserId: info.yUserId,
|
||||
userId: info.userId,
|
||||
userName: info.userName,
|
||||
gateDepartName: info.gateDepartName,
|
||||
typeName: info.typeName,
|
||||
access_token: maskToken(normalizeExternalAccessToken(info.access_token)),
|
||||
accessToken: maskToken(normalizeExternalAccessToken(info.accessToken)),
|
||||
};
|
||||
}
|
||||
|
||||
function logYUserLogin(step: string, payload: Record<string, unknown>): void {
|
||||
if (typeof console === "undefined") return;
|
||||
console.info(`[yUserId-login] ${step}`, payload);
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
@@ -95,28 +134,34 @@ function buildExternalUserInfo(
|
||||
}
|
||||
}
|
||||
next.yUserId = yUserId;
|
||||
const externalUserId =
|
||||
normalizeExternalUserId(result.id) ||
|
||||
normalizeExternalUserId(result.userId) ||
|
||||
normalizeExternalUserId(result.user_id) ||
|
||||
yUserId;
|
||||
next.userId = externalUserId;
|
||||
|
||||
const incomingAccessToken = normalizeExternalAccessToken(result.accessToken);
|
||||
const incomingAccessToken = readExternalAccessTokenValue(result);
|
||||
if (incomingAccessToken) {
|
||||
next.accessToken = incomingAccessToken;
|
||||
writeExternalAccessTokenFields(next, incomingAccessToken);
|
||||
return next;
|
||||
}
|
||||
|
||||
const queryAccessToken = normalizeExternalAccessToken(access_token);
|
||||
if (queryAccessToken) {
|
||||
next.accessToken = queryAccessToken;
|
||||
writeExternalAccessTokenFields(next, queryAccessToken);
|
||||
return next;
|
||||
}
|
||||
|
||||
const previousYUserId = readStoredYUserId(previous);
|
||||
const previousAccessToken =
|
||||
previousYUserId === yUserId ? normalizeExternalAccessToken(previous.accessToken) : "";
|
||||
const cachedAccessToken = normalizeExternalAccessToken(cached.accessToken);
|
||||
previousYUserId === yUserId ? readExternalAccessTokenValue(previous) : "";
|
||||
const cachedAccessToken = readExternalAccessTokenValue(cached);
|
||||
const fallbackAccessToken = previousAccessToken || cachedAccessToken;
|
||||
if (fallbackAccessToken) {
|
||||
next.accessToken = fallbackAccessToken;
|
||||
writeExternalAccessTokenFields(next, fallbackAccessToken);
|
||||
} else {
|
||||
delete next.accessToken;
|
||||
deleteExternalAccessTokenFields(next);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
@@ -134,8 +179,16 @@ export async function loginByYUserId(
|
||||
authStorageSet(TOOLS_TOKEN_STORAGE_KEY, token);
|
||||
}
|
||||
|
||||
const requestUrl = `${CONSUMER_URL}/cop/userDetailByYuserId`;
|
||||
logYUserLogin("request", {
|
||||
requestUrl,
|
||||
yUserId,
|
||||
hasToolsToken: Boolean(token),
|
||||
queryAccessToken: maskToken(normalizeExternalAccessToken(access_token)),
|
||||
});
|
||||
|
||||
const res = (await requestCrudService.get(
|
||||
`${CONSUMER_URL}/cop/userDetailByYuserId`,
|
||||
requestUrl,
|
||||
{ yuserId: yUserId },
|
||||
)) as UserDetailResponse;
|
||||
|
||||
@@ -143,12 +196,38 @@ export async function loginByYUserId(
|
||||
// 一并存入路由参数 yUserId,使「应用管理」里 source=yUserId 的路由参数能取到(与
|
||||
// source=userName/gateDepartName/typeName 直接取接口值统一走 userInfo[source])。
|
||||
// 注意:接口返回的 accessToken 优先;登录 URL 的 access_token 只在接口值为空时兜底。
|
||||
// The consumer API may return accessToken. URL access_token is only used as
|
||||
// a fallback when that API token is empty.
|
||||
const result = res.data ?? {};
|
||||
logYUserLogin("response", {
|
||||
keys: Object.keys(result),
|
||||
id: result.id,
|
||||
userId: result.userId,
|
||||
user_id: result.user_id,
|
||||
userName: result.userName,
|
||||
gateDepartName: result.gateDepartName,
|
||||
typeName: result.typeName,
|
||||
access_token: maskToken(normalizeExternalAccessToken(result.access_token)),
|
||||
accessToken: maskToken(normalizeExternalAccessToken(result.accessToken)),
|
||||
});
|
||||
const previous = readExternalUserInfo();
|
||||
const cached = readCachedYUserInfo(yUserId);
|
||||
const next = buildExternalUserInfo(yUserId, result, access_token, previous, cached);
|
||||
const externalUserId = normalizeExternalUserId(next.userId);
|
||||
if (externalUserId) {
|
||||
authStorageSet(EXTERNAL_USER_ID_STORAGE_KEY, externalUserId);
|
||||
}
|
||||
const externalAccessToken = readExternalAccessTokenValue(next);
|
||||
if (externalAccessToken) {
|
||||
writeStoredExternalAccessToken(externalAccessToken);
|
||||
}
|
||||
authStorageSet(EXTERNAL_USER_INFO_STORAGE_KEY, JSON.stringify(next));
|
||||
writeCachedYUserInfo(yUserId, next);
|
||||
logYUserLogin("stored", {
|
||||
userId: externalUserId,
|
||||
topLevelAccessToken: maskToken(readStoredExternalAccessTokenValue()),
|
||||
userInfo: summarizeUserInfo(next),
|
||||
});
|
||||
}
|
||||
|
||||
export function persistToolsToken(token: string): void {
|
||||
@@ -156,17 +235,18 @@ export function persistToolsToken(token: string): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* 把内网登录路由参数 access_token 合并写入 userInfo.accessToken。
|
||||
* 把内网登录路由参数 access_token 合并写入 userInfo.access_token / userInfo.accessToken。
|
||||
* 据此拼参的有:XK/PG 中任务 iframe(XK 用 accessToken、PG 用 access_token,二者均读
|
||||
* external-links.getAccessToken() = userInfo.accessToken);以及「应用管理」里
|
||||
* source=accessToken 的轻应用路由参数(resolveRouteParamValue 取 userInfo.accessToken)。
|
||||
* external-links.getAccessToken());以及「应用管理」里 source=access_token / accessToken
|
||||
* 的轻应用路由参数(resolveRouteParamValue 统一读两种字段)。
|
||||
* 用合并而非覆盖整对象,避免抹掉 loginByYUserId 从 consumer 拿到的用户信息。
|
||||
*/
|
||||
export function persistAccessToken(token: string): void {
|
||||
const trimmed = normalizeExternalAccessToken(token);
|
||||
if (!trimmed) return;
|
||||
const info = readExternalUserInfo();
|
||||
info.accessToken = trimmed;
|
||||
writeExternalAccessTokenFields(info, trimmed);
|
||||
writeStoredExternalAccessToken(trimmed);
|
||||
authStorageSet(EXTERNAL_USER_INFO_STORAGE_KEY, JSON.stringify(info));
|
||||
const yUserId = readStoredYUserId(info);
|
||||
if (yUserId) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Strategy 模块外部 iframe / 嵌入页链接(来自 Vite 环境变量) */
|
||||
|
||||
import { authStorageGet } from "@/core/auth/scoped-storage";
|
||||
import { normalizeExternalAccessToken } from "@/core/auth/external-access-token";
|
||||
import { readCurrentExternalAccessTokenValue } from "@/core/auth/external-access-token";
|
||||
|
||||
function trimTrailingSlash(url: string): string {
|
||||
return url.replace(/\/+$/, "");
|
||||
@@ -44,7 +44,7 @@ function readUserInfo(): Record<string, unknown> {
|
||||
|
||||
/** 当前登录态 accessToken(无则空串)。 */
|
||||
export function getAccessToken(): string {
|
||||
return normalizeExternalAccessToken(readUserInfo().accessToken);
|
||||
return readCurrentExternalAccessTokenValue(readUserInfo());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user