Files
leaudit-platform-frontend/app/api/login/oauth-client.ts
T

354 lines
10 KiB
TypeScript

/**
* OAuth2.0客户端类
* 用于处理IDaaS OAuth2.0认证流程
* 如果需要添加新的Token相关功能:
* 1. 优先考虑在 `TokenManager` 中添加
* 2. 如果需要新的网络请求,在 `OAuthClient` 中添加
*/
interface OAuthConfig {
serverUrl: string;
clientId: string;
clientSecret?: string; // 可选,客户端不需要,仅服务器端使用
redirectUri: string;
appId: string;
}
interface TokenResponse {
access_token: string;
token_type: string;
refresh_token: string;
expires_in: number;
scope: string;
jti: string;
}
interface UserInfoResponse {
success: boolean;
code: string;
message: string | null;
requestId: string;
data: {
sub: string;
ou_id: string;
nickname: string;
phone_number: string;
ou_name: string;
email: string;
username: string;
};
}
export class OAuthClient {
private config: OAuthConfig;
constructor(config: OAuthConfig) {
this.config = {
...config,
serverUrl: config.serverUrl.replace(/\/$/, '') // 移除末尾斜杠
};
// 🔍 仅在服务器端打印配置调试信息
if (typeof window === 'undefined') {
console.log('🔧 [服务器端] OAuthClient 初始化配置:', {
serverUrl: this.config.serverUrl,
clientId: this.config.clientId,
redirectUri: this.config.redirectUri,
appId: this.config.appId,
hasClientSecret: !!this.config.clientSecret,
clientSecretLength: this.config.clientSecret?.length || 0,
clientSecretPreview: this.config.clientSecret ? `${this.config.clientSecret.substring(0, 10)}...` : 'undefined'
});
}
}
/**
* 生成授权URL
* @param state 状态值,建议包含随机字符串和_idp后缀
* @returns 授权URL
*/
getAuthorizeUrl(state: string): string {
const params = new URLSearchParams({
response_type: 'code',
scope: 'read',
client_id: this.config.clientId,
redirect_uri: this.config.redirectUri,
state: state
});
// 只打印公开信息,不打印 client_secret(安全考虑)
console.log('🔧 OAuth授权URL配置:', {
serverUrl: this.config.serverUrl,
clientId: this.config.clientId,
redirectUri: this.config.redirectUri,
client_secret: this.config.clientSecret
});
return `${this.config.serverUrl}/oauth/authorize?${params.toString()}`;
}
/**
* 获取访问令牌
* @param code 授权码
* @returns 访问令牌响应
*/
async getAccessToken(code: string): Promise<TokenResponse | null> {
const url = `${this.config.serverUrl}/oauth/token`;
const data = new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: this.config.clientId,
client_secret: this.config.clientSecret || '', // 提供默认值避免类型错误
redirect_uri: this.config.redirectUri
});
console.log('🔧 请求Token URL:', url);
console.log('🔧 请求参数:', {
grant_type: 'authorization_code',
code: code,
client_id: this.config.clientId,
redirect_uri: this.config.redirectUri,
// 仅在服务器端打印 client_secret 状态
hasClientSecret: !!this.config.clientSecret,
clientSecretLength: this.config.clientSecret?.length || 0
});
try {
// 创建 AbortController 用于超时控制
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000); // 15秒超时
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: data,
signal: controller.signal
});
clearTimeout(timeoutId);
console.log('🔧 Token响应状态:', response.status, response.statusText);
if (!response.ok) {
const errorData = await response.json();
console.error('❌ 获取访问令牌失败:', {
status: response.status,
statusText: response.statusText,
errorData: errorData
});
return null;
}
const tokenResponse = await response.json() as TokenResponse;
console.log('✅ 获取访问令牌成功:', {
token_type: tokenResponse.token_type,
expires_in: tokenResponse.expires_in,
scope: tokenResponse.scope
});
return tokenResponse;
} catch (error) {
// 判断是否为超时错误
if (error instanceof Error && error.name === 'AbortError') {
console.error('❌ 获取访问令牌超时(15秒):', error.message);
} else {
console.error('❌ 获取访问令牌网络错误:', error);
}
return null;
}
}
/**
* 获取用户信息
* @param accessToken 访问令牌
* @returns 用户信息响应
*/
async getUserInfo(accessToken: string): Promise<UserInfoResponse | null> {
const url = `${this.config.serverUrl}/api/bff/v1.2/oauth2/userinfo`;
try {
// 创建 AbortController 用于超时控制
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000); // 15秒超时
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${accessToken}`
},
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
console.error('获取用户信息失败:', response.status, response.statusText);
return null;
}
return await response.json() as UserInfoResponse;
} catch (error) {
// 判断是否为超时错误
if (error instanceof Error && error.name === 'AbortError') {
console.error('❌ 获取用户信息超时(15秒):', error.message);
} else {
console.error('❌ 获取用户信息网络错误:', error);
}
return null;
}
}
/**
* 刷新访问令牌
* @param refreshToken 刷新令牌
* @returns 新的访问令牌响应
*/
async refreshAccessToken(refreshToken: string): Promise<TokenResponse | null> {
const url = `${this.config.serverUrl}/oauth/token`;
const data = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.config.clientId,
client_secret: this.config.clientSecret || '' // 提供默认值避免类型错误
});
console.log('🔧 [刷新Token] 请求参数状态:', {
hasClientSecret: !!this.config.clientSecret,
clientSecretLength: this.config.clientSecret?.length || 0,
refreshTokenLength: refreshToken?.length || 0
});
try {
// 创建 AbortController 用于超时控制
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000); // 15秒超时
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: data,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorData = await response.json();
console.error('刷新访问令牌失败:', errorData);
return null;
}
return await response.json() as TokenResponse;
} catch (error) {
// 判断是否为超时错误
if (error instanceof Error && error.name === 'AbortError') {
console.error('❌ 刷新访问令牌超时(15秒):', error.message);
} else {
console.error('❌ 刷新访问令牌网络错误:', error);
}
return null;
}
}
/**
* 单点登出
* @param accessToken 访问令牌
* @param redirectUrl 登出后重定向URL
* @returns 登出是否成功
*/
async logout(accessToken: string, redirectUrl: string): Promise<boolean> {
const url = `${this.config.serverUrl}/public/sp/slo/${this.config.appId}`;
const data = new URLSearchParams({
access_token: accessToken,
redirect_url: redirectUrl
});
try {
// 创建 AbortController 用于超时控制
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000); // 15秒超时
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: data,
signal: controller.signal
});
clearTimeout(timeoutId);
return response.ok;
} catch (error) {
// 判断是否为超时错误
if (error instanceof Error && error.name === 'AbortError') {
console.error('❌ 登出超时(15秒):', error.message);
} else {
console.error('❌ 登出失败:', error);
}
return false;
}
}
/**
* 生成随机状态值
* @returns 状态值字符串
*/
generateState(): string {
// 获取当前端口号,优先级:API_PORT_CONFIG > PORT > 默认值
let currentPort = process.env.API_PORT_CONFIG || process.env.PORT;
// 如果环境变量中没有端口号,尝试从浏览器location获取
if (!currentPort && typeof window !== 'undefined') {
currentPort = window.location.port;
}
// 如果仍然没有端口号,使用默认端口
if (!currentPort) {
currentPort = '51703'; // 默认端口
}
const randomStr = Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
const stateValue = `login${currentPort}_${randomStr}_idp`;
console.log(`生成状态值: ${stateValue} (端口: ${currentPort})`);
return stateValue;
}
}
/**
* OAuth2.0工具函数
*/
export const oauthUtils = {
/**
* 从URL中提取查询参数
* @param url URL字符串
* @returns 查询参数对象
*/
getQueryParams(url: string): Record<string, string> {
const params: Record<string, string> = {};
const urlObj = new URL(url);
for (const [key, value] of urlObj.searchParams) {
params[key] = value;
}
return params;
},
/**
* 验证状态值
* @param state 返回的状态值
* @param expectedState 期望的状态值
* @returns 是否匹配
*/
validateState(state: string, expectedState: string): boolean {
return state === expectedState;
},
};