565 lines
20 KiB
TypeScript
565 lines
20 KiB
TypeScript
/**
|
||
* 认证服务核心实现
|
||
*
|
||
* 这个文件包含了用户认证、会话管理、Token 刷新等核心功能的具体实现。
|
||
*
|
||
* 主要功能:
|
||
* - 基于 Cookie 的会话管理
|
||
* - OAuth2.0 Token 自动刷新
|
||
* - 用户登录状态检查
|
||
* - 会话创建和销毁
|
||
* - 用户信息保存到数据库
|
||
*
|
||
* 技术栈:
|
||
* - Remix Session Storage (Cookie-based)
|
||
* - OAuth2.0 Token Management
|
||
* - TypeScript 类型安全
|
||
*
|
||
*/
|
||
|
||
import { createCookieSessionStorage } from "@remix-run/node";
|
||
import { postgrestGet, postgrestPost, postgrestPut } from "../postgrest-client";
|
||
import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt";
|
||
import { OAUTH_CONFIG, API_BASE_URL } from "~/config/api-config";
|
||
import axios from 'axios';
|
||
|
||
/**
|
||
* 用户角色类型定义
|
||
*
|
||
* @typedef {string} UserRole
|
||
* @property {'common'} common - 普通用户,有基本的系统访问权限
|
||
* @property {'developer'} developer - 开发者/管理员,有完整的系统管理权限
|
||
*/
|
||
export type UserRole = string;
|
||
|
||
/**
|
||
* 用户信息接口,对应 sso_users 表结构
|
||
*/
|
||
export interface UserInfo {
|
||
sub: string; // IDaaS用户唯一标识
|
||
username?: string; // 显示用户名称/工号
|
||
nick_name?: string; // 用户真实姓名
|
||
nickname?: string; // OAuth返回的昵称字段
|
||
name?: string; // 用户姓名(通常映射到 nick_name)
|
||
phone_number?: string; // 手机号
|
||
email?: string; // 邮箱地址
|
||
ou_id?: string; // 所属组织单位ID
|
||
ou_name?: string; // 所属部门名称
|
||
status?: number; // 账户状态: 0=正常, 1=禁用
|
||
is_leader?: boolean; // 是否为部门负责人
|
||
area?: string; // 用户所属地区
|
||
id?: string | number; // 临时的用户id
|
||
user_id?: string | number;
|
||
user_role?: string;
|
||
// 🔑 组织信息字段(可能为null)
|
||
tenant_name?: string | null;
|
||
dep_name?: string | null;
|
||
dep_short_name?: string | null;
|
||
}
|
||
|
||
/**
|
||
* sso_users 表记录接口
|
||
*/
|
||
export interface SsoUser {
|
||
id?: string;
|
||
sub: string;
|
||
username: string;
|
||
nick_name: string;
|
||
phone_number?: string;
|
||
email?: string;
|
||
ou_id: string;
|
||
ou_name: string;
|
||
status: number;
|
||
is_leader: boolean;
|
||
area?: string;
|
||
created_at?: string;
|
||
updated_at?: string;
|
||
deleted_at?: string;
|
||
}
|
||
|
||
function compactUserInfoForSession(userInfo?: UserInfo, userRole?: string): UserInfo | undefined {
|
||
if (!userInfo) {
|
||
return undefined;
|
||
}
|
||
|
||
// Cookie Session 直接存整份 userInfo 很容易超过浏览器 4KB 限制;
|
||
// 服务端鉴权实际只依赖这几个核心字段,其余信息交给接口按需取回。
|
||
return {
|
||
user_id: userInfo.user_id,
|
||
sub: userInfo.sub,
|
||
username: userInfo.username,
|
||
nick_name: userInfo.nick_name || userInfo.nickname || userInfo.name,
|
||
ou_id: userInfo.ou_id,
|
||
ou_name: userInfo.ou_name,
|
||
is_leader: userInfo.is_leader,
|
||
area: userInfo.area,
|
||
user_role: userInfo.user_role || userRole,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 会话存储配置
|
||
*
|
||
* 使用 Remix 的 Cookie Session Storage 来管理用户会话。
|
||
* Cookie 存储方式的优势:
|
||
* - 服务器端控制,安全性高
|
||
* - 自动处理过期时间
|
||
* - 支持 HttpOnly,防止 XSS 攻击
|
||
*
|
||
* 配置说明:
|
||
* - name: Cookie 名称,用于标识会话
|
||
* - httpOnly: 防止客户端 JavaScript 访问,提高安全性
|
||
* - sameSite: CSRF 保护
|
||
* - secrets: 用于签名和加密 Cookie 内容
|
||
* - maxAge: 会话有效期,与 OAuth Token 过期时间保持一致
|
||
*/
|
||
export const sessionStorage = createCookieSessionStorage({
|
||
cookie: {
|
||
name: "__lgsession", // 登录会话 Cookie 名称
|
||
httpOnly: true, // 仅服务器端可访问,防止 XSS
|
||
path: "/", // Cookie 作用域为整个应用
|
||
sameSite: "lax", // CSRF 保护,允许顶级导航
|
||
secrets: ["s3cr3t"], // TODO: 应该从环境变量读取
|
||
maxAge: 60 * 60 * 8, // 8小时,确保大于等于JWT token最大有效期(通常为6小时)
|
||
secure: false, // 开发环境中禁用 HTTPS 要求
|
||
},
|
||
});
|
||
|
||
/**
|
||
* 获取会话对象
|
||
*
|
||
* 从请求中提取 Cookie 并获取对应的会话对象。
|
||
* 这是一个底层函数,通常不直接使用,而是通过 getUserSession 使用。
|
||
*
|
||
* @param request - Remix Request 对象,包含 HTTP 请求信息
|
||
* @returns 会话对象,可以用来读取和设置会话数据
|
||
*/
|
||
export async function getSession(request: Request) {
|
||
const cookie = request.headers.get("Cookie");
|
||
return sessionStorage.getSession(cookie);
|
||
}
|
||
|
||
/**
|
||
* 获取用户登录状态(带自动Token刷新)
|
||
*
|
||
* 这是认证系统的核心函数,负责:
|
||
* 1. 检查用户是否已登录
|
||
* 2. 验证 OAuth Token 是否有效
|
||
* 3. 自动刷新即将过期的 Token
|
||
* 4. 返回用户信息和认证状态
|
||
*
|
||
* Token 刷新机制:
|
||
* - 检查 access_token 是否即将过期(距离过期时间 < 5 分钟)
|
||
* - 如果即将过期,使用 refresh_token 自动获取新的 access_token
|
||
* - 如果刷新失败,则认为用户未登录
|
||
*
|
||
* @param request - Remix Request 对象
|
||
* @returns 包含以下字段的对象:
|
||
* - isAuthenticated: 是否已认证
|
||
* - userRole: 用户角色 ('common' | 'developer')
|
||
* - accessToken: OAuth 访问令牌
|
||
* - refreshToken: OAuth 刷新令牌
|
||
* - userInfo: 用户详细信息
|
||
* - isTokenExpired: Token 是否已过期
|
||
* - refreshedSession: 如果刷新了 Token,返回更新后的会话对象
|
||
*/
|
||
/**
|
||
* 生成前端JWT
|
||
* @param userInfo 用户信息
|
||
* @param expiresIn OAuth token过期时间(秒)
|
||
* @returns JWT字符串
|
||
*/
|
||
// async function generateFrontendJWT(userInfo: UserInfo, savedUserData: SsoUser, userRole: UserRole, expiresIn: number): Promise<string> {
|
||
// const jwtUserInfo: UserInfoForJWT = {
|
||
// sub: userInfo.sub,
|
||
// user_id: savedUserData.id!,
|
||
// username: savedUserData.username,
|
||
// nick_name: savedUserData.nick_name,
|
||
// email: savedUserData.email,
|
||
// phone_number: savedUserData.phone_number,
|
||
// ou_id: savedUserData.ou_id,
|
||
// ou_name: savedUserData.ou_name,
|
||
// is_leader: savedUserData.is_leader,
|
||
// user_role: userRole
|
||
// };
|
||
|
||
// return JWTUtils.generateJWT(jwtUserInfo, expiresIn);
|
||
// }
|
||
|
||
/**
|
||
* 创建包含JWT的用户信息对象
|
||
* @param userInfo OAuth用户信息
|
||
* @param savedUserData 数据库中保存的用户数据
|
||
* @param userRole 用户角色
|
||
* @param frontendJWT 前端JWT
|
||
* @returns 完整的用户信息对象
|
||
*/
|
||
// function createUserInfoWithJWT(userInfo: UserInfo, savedUserData: SsoUser, userRole: UserRole, frontendJWT: string) {
|
||
// return {
|
||
// // 保持与callback.tsx中enhancedUserInfo相同的数据结构
|
||
// sub: userInfo.sub,
|
||
// username: savedUserData.username,
|
||
// nick_name: savedUserData.nick_name,
|
||
// phone_number: savedUserData.phone_number,
|
||
// email: savedUserData.email,
|
||
// ou_id: savedUserData.ou_id,
|
||
// ou_name: savedUserData.ou_name,
|
||
// status: savedUserData.status,
|
||
// is_leader: savedUserData.is_leader,
|
||
// // 增强字段,与OAuth登录保持一致
|
||
// user_id: savedUserData.id,
|
||
// user_role: userRole,
|
||
// frontend_jwt: frontendJWT
|
||
// };
|
||
// }
|
||
|
||
export async function getUserSession(request: Request) {
|
||
const session = await getSession(request);
|
||
const isAuthenticated = session.get("isAuthenticated") === true;
|
||
const userRole = session.get("userRole") as UserRole;
|
||
const accessToken = session.get("accessToken");
|
||
const refreshToken = session.get("refreshToken");
|
||
const tokenIssuedAt = session.get("tokenIssuedAt");
|
||
let tokenExpiresIn = session.get("tokenExpiresIn");
|
||
const storedUserInfo = session.get("userInfo");
|
||
const userInfo = storedUserInfo
|
||
? {
|
||
...storedUserInfo,
|
||
role: storedUserInfo.role || storedUserInfo.user_role || userRole,
|
||
user_role: storedUserInfo.user_role || userRole,
|
||
}
|
||
: storedUserInfo;
|
||
const frontendJWT = session.get("frontendJWT");
|
||
|
||
// 🔑 检查是否是公共路径(不需要认证的路径)
|
||
const url = new URL(request.url);
|
||
const pathname = url.pathname;
|
||
const publicPaths = ['/login', '/favicon.ico', '/callback'];
|
||
const isPublicPath = publicPaths.some(path => pathname.startsWith(path));
|
||
|
||
let refreshedSession = null;
|
||
// let shouldRegenerateJWT = false;
|
||
|
||
// 🔑 新的统一过期检查逻辑
|
||
// 不区分 admin 和 OAuth 用户,所有用户都使用同样的过期检查
|
||
// 如果没有设置 tokenExpiresIn,给一个默认值(24小时)
|
||
if (!tokenExpiresIn) {
|
||
tokenExpiresIn = 86400; // 24小时(与后端登录接口返回的一致)
|
||
session.set("tokenExpiresIn", tokenExpiresIn);
|
||
refreshedSession = session;
|
||
}
|
||
|
||
// 🔑 简化:不再自动重新生成 JWT
|
||
// JWT 即将过期时,直接让用户重新登录
|
||
|
||
// 🔑 关键:验证 session 完整性
|
||
// 如果 isAuthenticated 为 true,但缺少关键数据(userInfo),则认为 session 无效
|
||
let finalIsAuthenticated = isAuthenticated;
|
||
|
||
if (finalIsAuthenticated) {
|
||
// 检查是否有用户信息
|
||
if (!userInfo) {
|
||
console.warn("⚠️ [getUserSession] Session 不完整:缺少 userInfo");
|
||
finalIsAuthenticated = false;
|
||
}
|
||
|
||
// 检查是否有前端 JWT
|
||
if (!frontendJWT) {
|
||
console.warn("⚠️ [getUserSession] Session 不完整:缺少 frontendJWT");
|
||
finalIsAuthenticated = false;
|
||
}
|
||
|
||
// 🔑 统一的 token 过期检查(所有用户类型)
|
||
// 使用签发时间 + 有效期 - 5分钟缓冲来判断是否需要重新登录
|
||
if (tokenIssuedAt && tokenExpiresIn) {
|
||
const now = Date.now(); // 当前时间(毫秒)
|
||
const expiresAt = tokenIssuedAt + (tokenExpiresIn * 1000); // 过期时间(毫秒)
|
||
const bufferTime = 5 * 60 * 1000; // 5分钟缓冲时间(毫秒)
|
||
|
||
// 如果当前时间 >= 过期时间 - 5分钟,则认为 token 即将过期或已过期
|
||
if (now >= expiresAt - bufferTime) {
|
||
const remainingSeconds = Math.floor((expiresAt - now) / 1000);
|
||
console.error(`❌ [getUserSession] Token 即将过期或已过期 (剩余: ${remainingSeconds} 秒)`);
|
||
finalIsAuthenticated = false;
|
||
}
|
||
} else if (!tokenIssuedAt) {
|
||
// 如果没有签发时间,认为 session 无效(可能是旧版本数据)
|
||
console.warn("⚠️ [getUserSession] Session 缺少 tokenIssuedAt,认为已过期");
|
||
finalIsAuthenticated = false;
|
||
}
|
||
}
|
||
|
||
// 🚨 统一的认证检查和重定向逻辑
|
||
if (!finalIsAuthenticated) {
|
||
// 如果是公共路径,不重定向,直接返回未认证状态
|
||
if (isPublicPath) {
|
||
// console.log("ℹ️ [getUserSession] 公共路径,允许未认证访问:", pathname);
|
||
return {
|
||
isAuthenticated: false,
|
||
userRole,
|
||
accessToken,
|
||
refreshToken,
|
||
userInfo,
|
||
refreshedSession,
|
||
frontendJWT
|
||
};
|
||
}
|
||
|
||
// 非公共路径且未认证,重定向到登录页
|
||
if (isAuthenticated) {
|
||
// Session 存在但已失效(token 过期或数据不完整)
|
||
console.error("❌ [getUserSession] Session 已失效,清除 session 并重定向到登录页");
|
||
const { redirect } = await import("@remix-run/node");
|
||
const destroyedSession = await sessionStorage.destroySession(session);
|
||
|
||
// 重定向到登录页,添加 expired=true 参数
|
||
throw redirect("/login?expired=true", {
|
||
headers: {
|
||
"Set-Cookie": destroyedSession
|
||
}
|
||
});
|
||
} else {
|
||
// Session 不存在(首次访问或已登出)
|
||
console.warn("⚠️ [getUserSession] 未登录,重定向到登录页");
|
||
const { redirect } = await import("@remix-run/node");
|
||
|
||
// 保存当前路径,登录后可以跳转回来
|
||
const redirectTo = pathname !== '/login' ? pathname : '/';
|
||
throw redirect(`/login?redirect=${encodeURIComponent(redirectTo)}`);
|
||
}
|
||
}
|
||
|
||
return {
|
||
isAuthenticated: finalIsAuthenticated,
|
||
userRole,
|
||
accessToken,
|
||
refreshToken,
|
||
userInfo,
|
||
refreshedSession, // 如果刷新了token,返回更新后的session
|
||
frontendJWT // 返回前端JWT
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 创建用户登录会话(完整版)
|
||
*
|
||
* 在用户成功登录后调用此函数来创建完整的会话并设置 Cookie。
|
||
* 这个函数支持完整的OAuth2.0登录流程,包括token管理和用户信息存储。
|
||
*
|
||
* 处理流程:
|
||
* 1. 创建新的会话对象
|
||
* 2. 设置认证状态、用户角色、token信息
|
||
* 3. 保存用户信息和前端JWT
|
||
* 4. 生成签名的 Cookie
|
||
* 5. 返回重定向响应并设置 Cookie
|
||
*
|
||
* @param params - 会话创建参数
|
||
* @returns HTTP 302 重定向响应,包含设置 Cookie 的头部
|
||
*/
|
||
export async function createUserSession(params: {
|
||
isAuthenticated: boolean;
|
||
userRole: string;
|
||
redirectTo: string;
|
||
accessToken?: string;
|
||
refreshToken?: string;
|
||
tokenExpiresIn?: number;
|
||
tokenIssuedAt?: number; // 🔑 新增:支持传递签发时间
|
||
userInfo?: UserInfo;
|
||
frontendJWT?: string;
|
||
}) {
|
||
const session = await sessionStorage.getSession();
|
||
|
||
// 基础认证信息
|
||
session.set("isAuthenticated", params.isAuthenticated);
|
||
session.set("userRole", params.userRole);
|
||
|
||
// OAuth token信息
|
||
if (params.accessToken) {
|
||
session.set("accessToken", params.accessToken);
|
||
}
|
||
if (params.refreshToken) {
|
||
session.set("refreshToken", params.refreshToken);
|
||
}
|
||
|
||
// 🔑 Token 时间信息(与 accessToken 独立,支持管理员登录)
|
||
// 只要有 tokenExpiresIn 或 tokenIssuedAt,就设置这些字段
|
||
if (params.tokenExpiresIn) {
|
||
session.set("tokenExpiresIn", params.tokenExpiresIn);
|
||
}
|
||
if (params.tokenIssuedAt !== undefined) {
|
||
// 优先使用传递的 tokenIssuedAt
|
||
session.set("tokenIssuedAt", params.tokenIssuedAt);
|
||
} else if (params.accessToken || params.frontendJWT) {
|
||
// 如果没有传递 tokenIssuedAt,但有 token(accessToken 或 frontendJWT),则使用当前时间
|
||
session.set("tokenIssuedAt", Date.now());
|
||
}
|
||
|
||
// 用户信息和JWT
|
||
if (params.userInfo) {
|
||
session.set("userInfo", compactUserInfoForSession(params.userInfo, params.userRole));
|
||
}
|
||
if (params.frontendJWT) {
|
||
session.set("frontendJWT", params.frontendJWT);
|
||
}
|
||
|
||
// 🔑 根据 tokenExpiresIn 动态设置 Cookie 的 maxAge
|
||
// 如果有 tokenExpiresIn,使用它作为 Cookie 有效期;否则使用默认值(8小时)
|
||
const cookieMaxAge = params.tokenExpiresIn || (60 * 60 * 8); // 默认8小时
|
||
// console.log("🍪 [createUserSession] Cookie maxAge:", cookieMaxAge, "秒 (", (cookieMaxAge / 3600).toFixed(2), "小时)");
|
||
|
||
const cookie = await sessionStorage.commitSession(session, {
|
||
maxAge: cookieMaxAge // 🔑 动态设置 Cookie 有效期
|
||
});
|
||
|
||
return new Response(null, {
|
||
status: 302, // HTTP 重定向状态码
|
||
headers: {
|
||
Location: params.redirectTo, // 重定向目标 URL
|
||
"Set-Cookie": cookie, // 设置会话 Cookie
|
||
},
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 创建用户登录会话(简化版)
|
||
*
|
||
* 在用户成功登录后调用此函数来创建会话并设置 Cookie。
|
||
* 这个函数通常在以下场景中使用:
|
||
* - 临时管理员登录
|
||
* - 测试用户登录
|
||
* - 其他简单认证方式成功后
|
||
*
|
||
* 处理流程:
|
||
* 1. 创建新的会话对象
|
||
* 2. 设置认证状态和用户角色
|
||
* 3. 生成签名的 Cookie
|
||
* 4. 返回重定向响应并设置 Cookie
|
||
*
|
||
* @param isAuthenticated - 是否已认证,通常为 true
|
||
* @param userRole - 用户角色,决定用户的权限级别
|
||
* @param redirectTo - 登录成功后重定向的 URL,默认为首页
|
||
* @returns HTTP 302 重定向响应,包含设置 Cookie 的头部
|
||
*/
|
||
export async function createSimpleUserSession(isAuthenticated: boolean, userRole: UserRole, redirectTo: string) {
|
||
const session = await sessionStorage.getSession();
|
||
session.set("isAuthenticated", isAuthenticated);
|
||
session.set("userRole", userRole);
|
||
|
||
const cookie = await sessionStorage.commitSession(session);
|
||
console.log("创建简化会话 - 设置Cookie:", !!cookie);
|
||
console.log("创建简化会话 - 用户角色:", userRole);
|
||
console.log("创建简化会话 - 重定向到:", redirectTo);
|
||
|
||
return new Response(null, {
|
||
status: 302, // HTTP 重定向状态码
|
||
headers: {
|
||
Location: redirectTo, // 重定向目标 URL
|
||
"Set-Cookie": cookie, // 设置会话 Cookie
|
||
},
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 销毁会话(用户登出)
|
||
*
|
||
* 当用户主动登出或会话失效时调用此函数。
|
||
*
|
||
* 处理流程:
|
||
* 1. 获取当前用户的会话和访问令牌
|
||
* 2. 调用 IDaaS 单点登出接口
|
||
* 3. 销毁本地会话数据(清除所有存储的信息)
|
||
* 4. 清除客户端的会话 Cookie
|
||
* 5. 重定向到登录页面
|
||
*
|
||
* 注意事项:
|
||
* - 这个函数会同时处理本地会话和 IDaaS 的单点登出
|
||
* - 即使 IDaaS 登出失败,也会清除本地会话
|
||
* - 销毁会话后,用户需要重新登录才能访问受保护的页面
|
||
*
|
||
* @param request - Remix Request 对象,用于获取当前会话
|
||
* @returns HTTP 302 重定向响应,清除 Cookie 并跳转到登录页
|
||
*/
|
||
export async function logout(request: Request) {
|
||
const session = await getSession(request);
|
||
|
||
// 获取访问令牌和应用ID,用于调用IDaaS单点登出
|
||
const accessToken = session.get("accessToken");
|
||
const appId = OAUTH_CONFIG.appId || 'idaasoauth2';
|
||
|
||
console.log("🚪 [Logout] 开始登出流程...");
|
||
console.log("🔑 [Logout] accessToken 存在:", !!accessToken);
|
||
console.log("📱 [Logout] appId:", appId);
|
||
|
||
// 如果存在访问令牌,调用IDaaS单点登出(仅 OAuth 登录用户)
|
||
if (accessToken && appId) {
|
||
console.log("🌐 [Logout] OAuth 用户,准备调用 IDaaS 单点登出...");
|
||
try {
|
||
await callIDaaSLogout(accessToken, appId);
|
||
console.log("✅ [Logout] IDaaS单点登出成功");
|
||
} catch (error) {
|
||
console.error("❌ [Logout] IDaaS单点登出失败:");
|
||
console.error(" 错误详情:", error);
|
||
if (error instanceof Error) {
|
||
console.error(" 错误消息:", error.message);
|
||
console.error(" 错误堆栈:", error.stack);
|
||
}
|
||
// 即使IDaaS登出失败,也继续清除本地会话
|
||
}
|
||
} else {
|
||
console.log("ℹ️ [Logout] 管理员登录用户,无需调用 IDaaS 登出");
|
||
}
|
||
|
||
return new Response(null, {
|
||
status: 302, // HTTP 重定向状态码
|
||
headers: {
|
||
Location: "/login", // 重定向到登录页面
|
||
"Set-Cookie": await sessionStorage.destroySession(session), // 清除会话 Cookie
|
||
},
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 调用IDaaS单点登出接口
|
||
*
|
||
* @param accessToken - 用户的访问令牌
|
||
* @param appId - 应用ID
|
||
* @returns Promise<void>
|
||
*/
|
||
async function callIDaaSLogout(accessToken: string, appId: string): Promise<void> {
|
||
const serverUrl = OAUTH_CONFIG.serverUrl || 'http://10.79.112.85';
|
||
const redirectUri = OAUTH_CONFIG.redirectUri || 'http://10.79.97.17/';
|
||
const logoutUrl = `${serverUrl}/public/sp/slo/${appId}`;
|
||
|
||
console.log("📡 [callIDaaSLogout] 准备发送登出请求:");
|
||
console.log(" 登出URL:", logoutUrl);
|
||
console.log(" 重定向URL:", redirectUri);
|
||
console.log(" accessToken:", accessToken ? `${accessToken.substring(0, 20)}...` : 'null');
|
||
|
||
const formData = new URLSearchParams();
|
||
formData.append('access_token', accessToken);
|
||
formData.append('redirect_url', encodeURIComponent(redirectUri));
|
||
|
||
try {
|
||
const response = await axios.post(logoutUrl, formData.toString(), {
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
},
|
||
});
|
||
|
||
console.log("✅ [callIDaaSLogout] IDaaS单点登出请求成功");
|
||
console.log(" 响应状态:", response.status);
|
||
console.log(" 响应数据:", response.data);
|
||
} catch (error) {
|
||
if (axios.isAxiosError(error)) {
|
||
console.error("❌ [callIDaaSLogout] 调用IDaaS登出接口失败:");
|
||
console.error(" HTTP状态:", error.response?.status);
|
||
console.error(" 状态文本:", error.response?.statusText);
|
||
console.error(" 响应数据:", error.response?.data);
|
||
console.error(" 请求配置:", error.config?.url, error.config?.method);
|
||
throw new Error(`IDaaS登出失败: ${error.response?.status} ${error.response?.statusText}`);
|
||
}
|
||
console.error("❌ [callIDaaSLogout] 调用IDaaS登出接口失败(非HTTP错误):", error);
|
||
throw error;
|
||
}
|
||
}
|