sso-login调整
This commit is contained in:
@@ -139,13 +139,13 @@ export default function HomePage() {
|
|||||||
color: '#04121f',
|
color: '#04121f',
|
||||||
background: 'linear-gradient(135deg, #00d4ff, #4f9eff)',
|
background: 'linear-gradient(135deg, #00d4ff, #4f9eff)',
|
||||||
}}>
|
}}>
|
||||||
{userInfo.userId.charAt(0).toUpperCase()}
|
{(userInfo.userName ?? userInfo.userId).charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<span style={{
|
<span style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
...(isDark ? { color: 'rgba(168,216,255,0.85)' } : { color: '#334155' }),
|
...(isDark ? { color: 'rgba(168,216,255,0.85)' } : { color: '#334155' }),
|
||||||
}}>
|
}}>
|
||||||
{userInfo.userId}
|
{userInfo.userName ?? userInfo.userId}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
|
|||||||
+4
-27
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { User, Lock, Building2, LogIn, AlertCircle, Loader2 } from 'lucide-react';
|
import { User, Lock, LogIn, AlertCircle, Loader2 } from 'lucide-react';
|
||||||
import StarBackground from '@/shared/components/StarBackground';
|
import StarBackground from '@/shared/components/StarBackground';
|
||||||
import { iamLogin, saveSession } from '@/shared/services/iamAuth';
|
import { iamLogin, saveSession } from '@/shared/services/iamAuth';
|
||||||
import { useTheme } from '@/shared/context/ThemeContext';
|
import { useTheme } from '@/shared/context/ThemeContext';
|
||||||
@@ -33,7 +33,6 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
const [userId, setUserId] = useState('');
|
const [userId, setUserId] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [tenantId, setTenantId] = useState('');
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
@@ -51,12 +50,8 @@ export default function LoginPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const result = await iamLogin({ userId: userId.trim(), password, tenantId: tenantId.trim() || undefined });
|
const result = await iamLogin({ userId: userId.trim(), password });
|
||||||
saveSession(result.token, {
|
saveSession(result.token, result.userInfo);
|
||||||
userId: result.userId,
|
|
||||||
tenantId: result.tenantId,
|
|
||||||
loginTime: new Date().toLocaleString('zh-CN'),
|
|
||||||
});
|
|
||||||
navigate('/', { replace: true });
|
navigate('/', { replace: true });
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
setError(ex instanceof Error ? ex.message : '登录失败,请稍后重试');
|
setError(ex instanceof Error ? ex.message : '登录失败,请稍后重试');
|
||||||
@@ -138,7 +133,7 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 20 }}>
|
||||||
<label style={{ display: 'block', fontSize: 13, marginBottom: 6, color: isDark ? 'rgba(168,216,255,0.8)' : '#475569' }}>
|
<label style={{ display: 'block', fontSize: 13, marginBottom: 6, color: isDark ? 'rgba(168,216,255,0.8)' : '#475569' }}>
|
||||||
密码
|
密码
|
||||||
</label>
|
</label>
|
||||||
@@ -157,24 +152,6 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 20 }}>
|
|
||||||
<label style={{ display: 'block', fontSize: 13, marginBottom: 6, color: isDark ? 'rgba(168,216,255,0.8)' : '#475569' }}>
|
|
||||||
租户 ID(可选)
|
|
||||||
</label>
|
|
||||||
<div style={{ position: 'relative' }}>
|
|
||||||
<Building2 size={16} style={iconStyle(isDark)} />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={tenantId}
|
|
||||||
onChange={(e) => setTenantId(e.target.value)}
|
|
||||||
placeholder="不填则自动匹配已授权租户"
|
|
||||||
style={inputStyle(isDark)}
|
|
||||||
onFocus={(e) => { e.currentTarget.style.borderColor = '#00d4ff'; }}
|
|
||||||
onBlur={(e) => { e.currentTarget.style.borderColor = isDark ? 'rgba(0,212,255,0.25)' : 'rgba(0,0,0,0.12)'; }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 8,
|
display: 'flex', alignItems: 'center', gap: 8,
|
||||||
|
|||||||
+64
-6
@@ -1,21 +1,47 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
|
import { AlertCircle } from 'lucide-react';
|
||||||
|
import { iamSsoLogin, saveSession } from '@/shared/services/iamAuth';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SSO 登录回调入口
|
* SSO 登录回调入口
|
||||||
* 接收 URL 参数 userToken,写入 sessionStorage 后跳转首页
|
*
|
||||||
|
* 流程:
|
||||||
|
* 1. 从 URL 参数获取 userToken
|
||||||
|
* 2. 调用 iamSsoLogin 完成:
|
||||||
|
* - POST /identity/token/refresh/app 刷新应用 token + 用户信息
|
||||||
|
* - POST /identity/login/info 获取登录详情
|
||||||
|
* - POST /tenant?appId=APPID 拉取租户列表,选默认租户
|
||||||
|
* - POST /identity/token/refresh/tenant 切换租户刷新 token
|
||||||
|
* 3. userInfo 写入 sessionStorage,跳转首页
|
||||||
*/
|
*/
|
||||||
const SSOLogin: React.FC = () => {
|
const SSOLogin: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const userToken = searchParams.get('userToken');
|
const userToken = searchParams.get('userToken');
|
||||||
if (userToken) {
|
if (!userToken) {
|
||||||
sessionStorage.setItem('userToken', userToken);
|
setError('URL 缺少 userToken 参数');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
navigate('/', { replace: true });
|
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const result = await iamSsoLogin(userToken);
|
||||||
|
if (cancelled) return;
|
||||||
|
saveSession(result.token, result.userInfo);
|
||||||
|
navigate('/', { replace: true });
|
||||||
|
} catch (ex) {
|
||||||
|
if (cancelled) return;
|
||||||
|
setError(ex instanceof Error ? ex.message : 'SSO 登录失败');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => { cancelled = true; };
|
||||||
}, [searchParams, navigate]);
|
}, [searchParams, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -26,8 +52,40 @@ const SSOLogin: React.FC = () => {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
background: '#0a1628',
|
background: '#0a1628',
|
||||||
|
color: '#a8d8ff',
|
||||||
|
fontFamily: "-apple-system, 'SF Pro Display', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif",
|
||||||
}}>
|
}}>
|
||||||
<Spin size="large" tip="正在登录..." />
|
{error ? (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16,
|
||||||
|
padding: '32px 40px',
|
||||||
|
borderRadius: 16,
|
||||||
|
background: 'rgba(239,68,68,0.08)',
|
||||||
|
border: '1px solid rgba(239,68,68,0.4)',
|
||||||
|
maxWidth: 460,
|
||||||
|
}}>
|
||||||
|
<AlertCircle size={36} color="#fca5a5" />
|
||||||
|
<div style={{ fontSize: 16, fontWeight: 600, color: '#fca5a5' }}>SSO 登录失败</div>
|
||||||
|
<div style={{ fontSize: 13, color: 'rgba(168,216,255,0.7)', textAlign: 'center', wordBreak: 'break-all' }}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/login', { replace: true })}
|
||||||
|
style={{
|
||||||
|
marginTop: 8, padding: '8px 20px', borderRadius: 8,
|
||||||
|
border: '1px solid rgba(0,212,255,0.4)', background: 'rgba(0,212,255,0.1)',
|
||||||
|
color: '#00d4ff', fontSize: 13, cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
前往登录页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
<div style={{ fontSize: 14, color: 'rgba(168,216,255,0.7)' }}>正在登录...</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+221
-32
@@ -3,23 +3,24 @@ import { JSEncrypt } from 'jsencrypt';
|
|||||||
/**
|
/**
|
||||||
* 鼎捷云 IAM 登录服务(纯前端实现)
|
* 鼎捷云 IAM 登录服务(纯前端实现)
|
||||||
*
|
*
|
||||||
* 加密流程(RSA + AES):
|
* 完整登录流程:
|
||||||
* 1. 客户端生成 RSA 密钥对
|
* 1. RSA+AES 加密链路获取 userToken(/api/iam/v2/identity/login)
|
||||||
* 2. GET /api/iam/v2/identity/publickey 获取服务端公钥
|
* 2. 拉取用户授权租户列表(POST /api/iam/v2/tenant?appId=APPID),默认选第一个
|
||||||
* 3. 用服务端公钥加密客户端公钥 → clientEncryptPublicKey
|
* 3. 切换租户刷新 token(POST /api/iam/v2/identity/token/refresh/tenant)
|
||||||
* 4. POST /api/iam/v2/identity/aeskey 获取被加密的 AES 密钥
|
* 4. 将完整用户信息(含 authoredUser)写入 sessionStorage
|
||||||
* 5. 用客户端私钥解密得到 aesKey(16 位)
|
|
||||||
* 6. AES-CBC 加密密码 → passwordHash
|
|
||||||
* 7. POST /api/iam/v2/identity/login 获取 userToken
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 代理路径(vite.config.ts 中 /iam-api → https://iam.digiwincloud.com.cn)
|
// 代理路径(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)
|
// 应用 apptoken(digi-middleware-auth-app)
|
||||||
export const APP_TOKEN =
|
export const APP_TOKEN =
|
||||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImRhdGEtYnVzaW5lc3MtZGVtbyIsInNpZCI6MH0.Spo64LstbWxjYNefVFAbEbgfjzZoQGNcqKSGuYUOCRk';
|
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImRhdGEtYnVzaW5lc3MtZGVtbyIsInNpZCI6MH0.Spo64LstbWxjYNefVFAbEbgfjzZoQGNcqKSGuYUOCRk';
|
||||||
|
|
||||||
|
// 应用 ID(用于租户列表查询)
|
||||||
|
export const APPID = 'data-business-demo';
|
||||||
|
|
||||||
// IAM AES 加密固定 IV(16 字节)
|
// IAM AES 加密固定 IV(16 字节)
|
||||||
const AES_IV = 'ghUb#er57HBh(u%g';
|
const AES_IV = 'ghUb#er57HBh(u%g';
|
||||||
|
|
||||||
@@ -48,8 +49,8 @@ async function aesEncryptToBase64(plainText: string, aesKey: string): Promise<st
|
|||||||
return btoa(bin);
|
return btoa(bin);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通用请求头 */
|
/** 应用层请求头(含 apptoken) */
|
||||||
function iamHeaders(extra?: Record<string, string>): Record<string, string> {
|
function appHeaders(extra?: Record<string, string>): Record<string, string> {
|
||||||
return {
|
return {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'digi-middleware-auth-app': APP_TOKEN,
|
'digi-middleware-auth-app': APP_TOKEN,
|
||||||
@@ -57,6 +58,25 @@ function iamHeaders(extra?: Record<string, string>): Record<string, string> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 用户鉴权请求头(含 apptoken + usertoken) */
|
||||||
|
function userHeaders(userToken: string, extra?: Record<string, string>): Record<string, string> {
|
||||||
|
return appHeaders({
|
||||||
|
'digi-middleware-auth-user': userToken,
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从任意对象中尝试提取 token 字符串 */
|
||||||
|
function pickToken(obj: Record<string, unknown>): string | undefined {
|
||||||
|
if (typeof obj.token === 'string') return obj.token;
|
||||||
|
if (typeof obj.userToken === 'string') return obj.userToken;
|
||||||
|
const data = obj.data as Record<string, unknown> | undefined;
|
||||||
|
if (data && typeof data.token === 'string') return data.token;
|
||||||
|
const result = obj.result as Record<string, unknown> | undefined;
|
||||||
|
if (result && typeof result.token === 'string') return result.token;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IamLoginParams {
|
export interface IamLoginParams {
|
||||||
userId: string;
|
userId: string;
|
||||||
password: string;
|
password: string;
|
||||||
@@ -64,15 +84,17 @@ export interface IamLoginParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IamLoginResult {
|
export interface IamLoginResult {
|
||||||
|
/** 最终 userToken(经 refresh/tenant 刷新后) */
|
||||||
token: string;
|
token: string;
|
||||||
|
/** userId */
|
||||||
userId: string;
|
userId: string;
|
||||||
tenantId: string;
|
/** 完整用户信息(含 login 原始返回 + authoredUser + 默认租户) */
|
||||||
raw: Record<string, unknown>;
|
userInfo: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IAM 普通登录(identityType: query)
|
* IAM 普通登录(identityType: query)
|
||||||
* 支持用户 id / 手机号 / 邮箱
|
* 完整流程:加密登录 → 拉取租户列表 → 切换默认租户刷新 token
|
||||||
*/
|
*/
|
||||||
export async function iamLogin({ userId, password, tenantId }: IamLoginParams): Promise<IamLoginResult> {
|
export async function iamLogin({ userId, password, tenantId }: IamLoginParams): Promise<IamLoginResult> {
|
||||||
// 1. 客户端生成 RSA 密钥对(1024)
|
// 1. 客户端生成 RSA 密钥对(1024)
|
||||||
@@ -82,7 +104,7 @@ export async function iamLogin({ userId, password, tenantId }: IamLoginParams):
|
|||||||
const clientPublicKeyB64 = stripPem(client.getPublicKey());
|
const clientPublicKeyB64 = stripPem(client.getPublicKey());
|
||||||
|
|
||||||
// 2. 获取服务端公钥
|
// 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})`);
|
if (!pkRes.ok) throw new Error(`获取服务端公钥失败 (HTTP ${pkRes.status})`);
|
||||||
const pkJson = await pkRes.json();
|
const pkJson = await pkRes.json();
|
||||||
const serverPublicKey: string = pkJson.publicKey;
|
const serverPublicKey: string = pkJson.publicKey;
|
||||||
@@ -95,9 +117,9 @@ export async function iamLogin({ userId, password, tenantId }: IamLoginParams):
|
|||||||
if (!clientEncryptPublicKey) throw new Error('加密客户端公钥失败');
|
if (!clientEncryptPublicKey) throw new Error('加密客户端公钥失败');
|
||||||
|
|
||||||
// 4. 获取加密的 AES 密钥
|
// 4. 获取加密的 AES 密钥
|
||||||
const aesRes = await fetch(`${IAM_BASE}/aeskey`, {
|
const aesRes = await fetch(`${IAM_IDENTITY_BASE}/aeskey`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: iamHeaders(),
|
headers: appHeaders(),
|
||||||
body: JSON.stringify({ clientEncryptPublicKey }),
|
body: JSON.stringify({ clientEncryptPublicKey }),
|
||||||
});
|
});
|
||||||
if (!aesRes.ok) throw new Error(`获取 AES 密钥失败 (HTTP ${aesRes.status})`);
|
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);
|
const passwordHash = await aesEncryptToBase64(password, aesKey);
|
||||||
|
|
||||||
// 7. 登录
|
// 7. 登录
|
||||||
const body: Record<string, string> = {
|
const loginBody: Record<string, string> = {
|
||||||
userId,
|
userId,
|
||||||
passwordHash,
|
passwordHash,
|
||||||
clientEncryptPublicKey,
|
clientEncryptPublicKey,
|
||||||
identityType: 'query',
|
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',
|
method: 'POST',
|
||||||
headers: iamHeaders(),
|
headers: appHeaders(),
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(loginBody),
|
||||||
});
|
});
|
||||||
const loginJson = await loginRes.json().catch(() => ({}));
|
const loginJson = (await loginRes.json().catch(() => ({}))) as Record<string, unknown>;
|
||||||
|
|
||||||
const token: string | undefined = loginJson.token ?? loginJson.data?.token ?? loginJson.userToken;
|
const initialToken = pickToken(loginJson);
|
||||||
if (!loginRes.ok || !token) {
|
if (!loginRes.ok || !initialToken) {
|
||||||
const msg = loginJson.message || loginJson.msg || loginJson.error || `HTTP ${loginRes.status}`;
|
const msg = loginJson.message || loginJson.msg || loginJson.error || `HTTP ${loginRes.status}`;
|
||||||
throw new Error(`登录失败: ${msg}`);
|
throw new Error(`登录失败: ${msg}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
// 8. 拉取用户授权租户列表 + 切换默认租户
|
||||||
token,
|
const tenantCtx = await switchDefaultTenant(initialToken);
|
||||||
|
|
||||||
|
// 9. 组装完整 userInfo:
|
||||||
|
// - authoredUser 内容平铺到顶层(sid/userName/email/currTenantList 等)
|
||||||
|
// - token 使用最终刷新后的 token
|
||||||
|
// - 保留 isLoggedin 标识
|
||||||
|
const userInfo: Record<string, unknown> = {
|
||||||
|
...(tenantCtx.authoredUser ?? {}),
|
||||||
|
...(loginJson ?? {}),
|
||||||
userId,
|
userId,
|
||||||
tenantId: loginJson.tenantId ?? tenantId ?? '',
|
token: tenantCtx.token,
|
||||||
raw: loginJson,
|
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<string, unknown>;
|
||||||
|
currTenantList?: unknown[];
|
||||||
|
}> {
|
||||||
|
let finalToken = userToken;
|
||||||
|
let authoredUser: Record<string, unknown> | 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<string, unknown>;
|
||||||
|
|
||||||
|
// 兼容数组 / {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<string, unknown>)?.isDefault === true)
|
||||||
|
?? tenants[0]) as Record<string, unknown>;
|
||||||
|
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<string, unknown>;
|
||||||
|
const refreshedToken = pickToken(refreshJson);
|
||||||
|
if (refreshedToken) finalToken = refreshedToken;
|
||||||
|
// authoredUser 平铺到 userInfo 顶层
|
||||||
|
if (refreshJson.authoredUser && typeof refreshJson.authoredUser === 'object') {
|
||||||
|
authoredUser = refreshJson.authoredUser as Record<string, unknown>;
|
||||||
|
} else if (refreshJson.data && typeof refreshJson.data === 'object'
|
||||||
|
&& (refreshJson.data as Record<string, unknown>).authoredUser) {
|
||||||
|
authoredUser = (refreshJson.data as Record<string, unknown>).authoredUser as Record<string, unknown>;
|
||||||
|
} 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<IamLoginResult> {
|
||||||
|
// 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<string, unknown>;
|
||||||
|
const appRefreshedToken = pickToken(refreshAppJson) ?? initialUserToken;
|
||||||
|
|
||||||
|
// 2. login/info:获取登录详情
|
||||||
|
let loginInfoJson: Record<string, unknown> = {};
|
||||||
|
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<string, unknown>;
|
||||||
|
}
|
||||||
|
} 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<string, unknown> = {
|
||||||
|
...(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 {
|
export interface SessionUserInfo {
|
||||||
userId: string;
|
userId: string;
|
||||||
tenantId: string;
|
userName?: string;
|
||||||
loginTime: 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<string, unknown>): void {
|
||||||
sessionStorage.setItem(KEY_USER_TOKEN, token);
|
sessionStorage.setItem(KEY_USER_TOKEN, token);
|
||||||
sessionStorage.setItem(KEY_USER_INFO, JSON.stringify(info));
|
sessionStorage.setItem(KEY_USER_INFO, JSON.stringify(info));
|
||||||
sessionStorage.setItem(KEY_APP_TOKEN, APP_TOKEN);
|
sessionStorage.setItem(KEY_APP_TOKEN, APP_TOKEN);
|
||||||
|
|||||||
Reference in New Issue
Block a user