211 lines
4.9 KiB
TypeScript
211 lines
4.9 KiB
TypeScript
/**
|
|
* 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<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
|
|
});
|
|
|
|
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<UserInfoResponse | null> {
|
|
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<boolean> {
|
|
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<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;
|
|
},
|
|
|
|
/**
|
|
* 检查访问令牌是否过期
|
|
* @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;
|
|
}
|
|
};
|