354 lines
10 KiB
TypeScript
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;
|
|
},
|
|
|
|
|
|
};
|