diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 7fe1171..07507b4 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -139,13 +139,13 @@ export default function HomePage() { color: '#04121f', background: 'linear-gradient(135deg, #00d4ff, #4f9eff)', }}> - {userInfo.userId.charAt(0).toUpperCase()} + {(userInfo.userName ?? userInfo.userId).charAt(0).toUpperCase()} - {userInfo.userId} + {userInfo.userName ?? userInfo.userId} + + ) : ( +
+ +
正在登录...
+
+ )} ); }; diff --git a/src/shared/services/iamAuth.ts b/src/shared/services/iamAuth.ts index bca6eb5..ca268fd 100644 --- a/src/shared/services/iamAuth.ts +++ b/src/shared/services/iamAuth.ts @@ -3,23 +3,24 @@ import { JSEncrypt } from 'jsencrypt'; /** * 鼎捷云 IAM 登录服务(纯前端实现) * - * 加密流程(RSA + AES): - * 1. 客户端生成 RSA 密钥对 - * 2. GET /api/iam/v2/identity/publickey 获取服务端公钥 - * 3. 用服务端公钥加密客户端公钥 → clientEncryptPublicKey - * 4. POST /api/iam/v2/identity/aeskey 获取被加密的 AES 密钥 - * 5. 用客户端私钥解密得到 aesKey(16 位) - * 6. AES-CBC 加密密码 → passwordHash - * 7. POST /api/iam/v2/identity/login 获取 userToken + * 完整登录流程: + * 1. RSA+AES 加密链路获取 userToken(/api/iam/v2/identity/login) + * 2. 拉取用户授权租户列表(POST /api/iam/v2/tenant?appId=APPID),默认选第一个 + * 3. 切换租户刷新 token(POST /api/iam/v2/identity/token/refresh/tenant) + * 4. 将完整用户信息(含 authoredUser)写入 sessionStorage */ // 代理路径(vite.config.ts 中 /iam-api → https://iam.digiwincloud.com.cn) -const IAM_BASE = '/iam-api/api/iam/v2/identity'; +const IAM_API_BASE = '/iam-api/api/iam/v2'; +const IAM_IDENTITY_BASE = `${IAM_API_BASE}/identity`; // 应用 apptoken(digi-middleware-auth-app) export const APP_TOKEN = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImRhdGEtYnVzaW5lc3MtZGVtbyIsInNpZCI6MH0.Spo64LstbWxjYNefVFAbEbgfjzZoQGNcqKSGuYUOCRk'; +// 应用 ID(用于租户列表查询) +export const APPID = 'data-business-demo'; + // IAM AES 加密固定 IV(16 字节) const AES_IV = 'ghUb#er57HBh(u%g'; @@ -48,8 +49,8 @@ async function aesEncryptToBase64(plainText: string, aesKey: string): Promise): Record { +/** 应用层请求头(含 apptoken) */ +function appHeaders(extra?: Record): Record { return { 'Content-Type': 'application/json', 'digi-middleware-auth-app': APP_TOKEN, @@ -57,6 +58,25 @@ function iamHeaders(extra?: Record): Record { }; } +/** 用户鉴权请求头(含 apptoken + usertoken) */ +function userHeaders(userToken: string, extra?: Record): Record { + return appHeaders({ + 'digi-middleware-auth-user': userToken, + ...extra, + }); +} + +/** 从任意对象中尝试提取 token 字符串 */ +function pickToken(obj: Record): string | undefined { + if (typeof obj.token === 'string') return obj.token; + if (typeof obj.userToken === 'string') return obj.userToken; + const data = obj.data as Record | undefined; + if (data && typeof data.token === 'string') return data.token; + const result = obj.result as Record | undefined; + if (result && typeof result.token === 'string') return result.token; + return undefined; +} + export interface IamLoginParams { userId: string; password: string; @@ -64,15 +84,17 @@ export interface IamLoginParams { } export interface IamLoginResult { + /** 最终 userToken(经 refresh/tenant 刷新后) */ token: string; + /** userId */ userId: string; - tenantId: string; - raw: Record; + /** 完整用户信息(含 login 原始返回 + authoredUser + 默认租户) */ + userInfo: Record; } /** * IAM 普通登录(identityType: query) - * 支持用户 id / 手机号 / 邮箱 + * 完整流程:加密登录 → 拉取租户列表 → 切换默认租户刷新 token */ export async function iamLogin({ userId, password, tenantId }: IamLoginParams): Promise { // 1. 客户端生成 RSA 密钥对(1024) @@ -82,7 +104,7 @@ export async function iamLogin({ userId, password, tenantId }: IamLoginParams): const clientPublicKeyB64 = stripPem(client.getPublicKey()); // 2. 获取服务端公钥 - const pkRes = await fetch(`${IAM_BASE}/publickey`, { headers: iamHeaders() }); + const pkRes = await fetch(`${IAM_IDENTITY_BASE}/publickey`, { headers: appHeaders() }); if (!pkRes.ok) throw new Error(`获取服务端公钥失败 (HTTP ${pkRes.status})`); const pkJson = await pkRes.json(); const serverPublicKey: string = pkJson.publicKey; @@ -95,9 +117,9 @@ export async function iamLogin({ userId, password, tenantId }: IamLoginParams): if (!clientEncryptPublicKey) throw new Error('加密客户端公钥失败'); // 4. 获取加密的 AES 密钥 - const aesRes = await fetch(`${IAM_BASE}/aeskey`, { + const aesRes = await fetch(`${IAM_IDENTITY_BASE}/aeskey`, { method: 'POST', - headers: iamHeaders(), + headers: appHeaders(), body: JSON.stringify({ clientEncryptPublicKey }), }); if (!aesRes.ok) throw new Error(`获取 AES 密钥失败 (HTTP ${aesRes.status})`); @@ -114,32 +136,190 @@ export async function iamLogin({ userId, password, tenantId }: IamLoginParams): const passwordHash = await aesEncryptToBase64(password, aesKey); // 7. 登录 - const body: Record = { + const loginBody: Record = { userId, passwordHash, clientEncryptPublicKey, identityType: 'query', }; - if (tenantId) body.tenantId = tenantId; + if (tenantId) loginBody.tenantId = tenantId; - const loginRes = await fetch(`${IAM_BASE}/login`, { + const loginRes = await fetch(`${IAM_IDENTITY_BASE}/login`, { method: 'POST', - headers: iamHeaders(), - body: JSON.stringify(body), + headers: appHeaders(), + body: JSON.stringify(loginBody), }); - const loginJson = await loginRes.json().catch(() => ({})); + const loginJson = (await loginRes.json().catch(() => ({}))) as Record; - const token: string | undefined = loginJson.token ?? loginJson.data?.token ?? loginJson.userToken; - if (!loginRes.ok || !token) { + const initialToken = pickToken(loginJson); + if (!loginRes.ok || !initialToken) { const msg = loginJson.message || loginJson.msg || loginJson.error || `HTTP ${loginRes.status}`; throw new Error(`登录失败: ${msg}`); } - return { - token, + // 8. 拉取用户授权租户列表 + 切换默认租户 + const tenantCtx = await switchDefaultTenant(initialToken); + + // 9. 组装完整 userInfo: + // - authoredUser 内容平铺到顶层(sid/userName/email/currTenantList 等) + // - token 使用最终刷新后的 token + // - 保留 isLoggedin 标识 + const userInfo: Record = { + ...(tenantCtx.authoredUser ?? {}), + ...(loginJson ?? {}), userId, - tenantId: loginJson.tenantId ?? tenantId ?? '', - raw: loginJson, + token: tenantCtx.token, + isLoggedin: true, + ...(tenantCtx.currTenantList ? { currTenantList: tenantCtx.currTenantList } : {}), + }; + + return { + token: tenantCtx.token, + userId, + userInfo, + }; +} + +/** + * 切换默认租户(公用流程) + * + * 1. POST /api/iam/v2/tenant?appId=APPID 拉取租户列表 + * 2. 优先选 isDefault=true 的租户,否则取第一个 + * 3. POST /api/iam/v2/identity/token/refresh/tenant body={tenantSid} + * 4. 返回刷新后的 token + authoredUser + currTenantList + * + * 异常不抛出,回退到传入的 userToken + */ +export async function switchDefaultTenant( + userToken: string, +): Promise<{ + token: string; + authoredUser?: Record; + currTenantList?: unknown[]; +}> { + let finalToken = userToken; + let authoredUser: Record | undefined; + let currTenantList: unknown[] | undefined; + + try { + const tenantRes = await fetch(`${IAM_API_BASE}/tenant?appId=${encodeURIComponent(APPID)}`, { + method: 'POST', + headers: userHeaders(userToken), + }); + if (!tenantRes.ok) throw new Error(`获取租户列表失败 (HTTP ${tenantRes.status})`); + const tenantJson = (await tenantRes.json().catch(() => ({}))) as Record; + + // 兼容数组 / {data:[]} / {list:[]} / {result:[]} + let tenants: unknown[] = []; + if (Array.isArray(tenantJson)) { + tenants = tenantJson; + } else if (Array.isArray(tenantJson.data)) { + tenants = tenantJson.data as unknown[]; + } else if (Array.isArray(tenantJson.list)) { + tenants = tenantJson.list as unknown[]; + } else if (Array.isArray(tenantJson.result)) { + tenants = tenantJson.result as unknown[]; + } + + if (tenants.length > 0) { + currTenantList = tenants; + // 优先选 isDefault=true 的租户,否则取第一个 + const defaultTenant = (tenants.find((t) => (t as Record)?.isDefault === true) + ?? tenants[0]) as Record; + const tenantSid = (defaultTenant.sid as number | string | undefined) + ?? (defaultTenant.tenantSid as number | string | undefined) + ?? (defaultTenant.id as number | string | undefined); + + if (tenantSid !== undefined && tenantSid !== null) { + // 切换默认租户,刷新 token + const refreshRes = await fetch(`${IAM_IDENTITY_BASE}/token/refresh/tenant`, { + method: 'POST', + headers: userHeaders(userToken), + body: JSON.stringify({ tenantSid }), + }); + if (refreshRes.ok) { + const refreshJson = (await refreshRes.json().catch(() => ({}))) as Record; + const refreshedToken = pickToken(refreshJson); + if (refreshedToken) finalToken = refreshedToken; + // authoredUser 平铺到 userInfo 顶层 + if (refreshJson.authoredUser && typeof refreshJson.authoredUser === 'object') { + authoredUser = refreshJson.authoredUser as Record; + } else if (refreshJson.data && typeof refreshJson.data === 'object' + && (refreshJson.data as Record).authoredUser) { + authoredUser = (refreshJson.data as Record).authoredUser as Record; + } else { + // 整个 refresh 返回作为 authoredUser(兼容字段直接在顶层) + authoredUser = refreshJson; + } + } + } + } + } catch (ex) { + // 租户切换失败不阻断登录,仍使用原 token + console.warn('[IAM] 租户切换流程异常,将使用原 token', ex); + } + + return { token: finalToken, authoredUser, currTenantList }; +} + +/** + * SSO 登录(基于外部传入的 userToken) + * + * 流程: + * 1. POST /api/iam/v2/identity/token/refresh/app 刷新应用 token + 用户信息 + * 2. POST /api/iam/v2/identity/login/info 获取登录详情 + * 3. 调用 switchDefaultTenant 拉取租户列表 + 切换默认租户 + * 4. 组装完整 userInfo(authoredUser 平铺到顶层) + */ +export async function iamSsoLogin(initialUserToken: string): Promise { + // 1. token/refresh/app:刷新应用 token + const refreshAppRes = await fetch(`${IAM_IDENTITY_BASE}/token/refresh/app`, { + method: 'POST', + headers: userHeaders(initialUserToken), + }); + if (!refreshAppRes.ok) throw new Error(`SSO token 刷新失败 (HTTP ${refreshAppRes.status})`); + const refreshAppJson = (await refreshAppRes.json().catch(() => ({}))) as Record; + const appRefreshedToken = pickToken(refreshAppJson) ?? initialUserToken; + + // 2. login/info:获取登录详情 + let loginInfoJson: Record = {}; + try { + const infoRes = await fetch(`${IAM_IDENTITY_BASE}/login/info`, { + method: 'POST', + headers: userHeaders(appRefreshedToken), + }); + if (infoRes.ok) { + loginInfoJson = (await infoRes.json().catch(() => ({}))) as Record; + } + } catch (ex) { + console.warn('[IAM] login/info 调用异常', ex); + } + + // 3. 切换默认租户 + const tenantCtx = await switchDefaultTenant(appRefreshedToken); + + // 4. 组装完整 userInfo + // 优先级:tenantCtx.authoredUser > refreshAppJson > loginInfoJson + // userId 从 loginInfo/refreshApp 中提取 + const userId = (loginInfoJson.userId as string) + ?? (refreshAppJson.userId as string) + ?? (tenantCtx.authoredUser?.userId as string) + ?? ''; + + const userInfo: Record = { + ...(tenantCtx.authoredUser ?? {}), + ...(refreshAppJson ?? {}), + ...(loginInfoJson ?? {}), + userId, + token: tenantCtx.token, + isLoggedin: true, + ...(tenantCtx.currTenantList ? { currTenantList: tenantCtx.currTenantList } : {}), + }; + + return { + token: tenantCtx.token, + userId, + userInfo, }; } @@ -151,12 +331,21 @@ const KEY_APP_TOKEN = 'digi-middleware-auth-app'; export interface SessionUserInfo { userId: string; - tenantId: string; - loginTime: string; + userName?: string; + token: string; + tenantId?: string; + tenantName?: string; + tenantSid?: number; + sid?: number; + email?: string; + telephone?: string; + isLoggedin?: boolean; + currTenantList?: unknown[]; + [key: string]: unknown; } /** 保存登录会话 */ -export function saveSession(token: string, info: SessionUserInfo): void { +export function saveSession(token: string, info: Record): void { sessionStorage.setItem(KEY_USER_TOKEN, token); sessionStorage.setItem(KEY_USER_INFO, JSON.stringify(info)); sessionStorage.setItem(KEY_APP_TOKEN, APP_TOKEN);