/** * 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(/\/$/, '') // 移除末尾斜杠 }; } /** * 生成授权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 }); return `${this.config.serverUrl}/oauth/authorize?${params.toString()}`; } /** * 获取访问令牌 * @param code 授权码 * @returns 访问令牌响应 */ async getAccessToken(code: string): Promise { console.log('🔧 OAuth配置信息:', { serverUrl: this.config.serverUrl, clientId: this.config.clientId, redirectUri: this.config.redirectUri }); 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 }); try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data }); 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) { console.error('❌ 获取访问令牌网络错误:', error); return null; } } /** * 获取用户信息 * @param accessToken 访问令牌 * @returns 用户信息响应 */ async getUserInfo(accessToken: string): Promise { const url = `${this.config.serverUrl}/api/bff/v1.2/oauth2/userinfo`; try { const response = await fetch(url, { headers: { 'Authorization': `Bearer ${accessToken}` } }); if (!response.ok) { console.error('获取用户信息失败:', response.status, response.statusText); return null; } return await response.json() as UserInfoResponse; } catch (error) { console.error('获取用户信息网络错误:', error); return null; } } /** * 刷新访问令牌 * @param refreshToken 刷新令牌 * @returns 新的访问令牌响应 */ async refreshAccessToken(refreshToken: string): Promise { 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 }); try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data }); if (!response.ok) { const errorData = await response.json(); console.error('刷新访问令牌失败:', errorData); return null; } return await response.json() as TokenResponse; } catch (error) { console.error('刷新访问令牌网络错误:', error); return null; } } /** * 单点登出 * @param accessToken 访问令牌 * @param redirectUrl 登出后重定向URL * @returns 登出是否成功 */ async logout(accessToken: string, redirectUrl: string): Promise { const url = `${this.config.serverUrl}/public/sp/slo/${this.config.appId}`; const data = new URLSearchParams({ access_token: accessToken, redirect_url: redirectUrl }); try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data }); return response.ok; } catch (error) { 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 { const params: Record = {}; 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; }, };