/** * OAuth2.0客户端类 * 用于处理IDaaS OAuth2.0认证流程 */ 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 { 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 }); 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 访问令牌 * @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 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 { const randomStr = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); return `${randomStr}_idp`; } } /** * 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; }, /** * 检查访问令牌是否过期 * @param tokenInfo 令牌信息 * @param issuedAt 令牌颁发时间戳 * @returns 是否过期 */ isTokenExpired(tokenInfo: TokenResponse, issuedAt: number): boolean { const now = Date.now(); const expiresAt = issuedAt + (tokenInfo.expires_in * 1000); return now >= expiresAt; } };