281 lines
11 KiB
C#
281 lines
11 KiB
C#
using IdentityModel.OidcClient;
|
||
using IdentityModel.OidcClient.Results;
|
||
using System.Net.Http.Headers;
|
||
using System.Text.Json;
|
||
using AudioWallpaper.Sso.Exceptions;
|
||
|
||
namespace AudioWallpaper.SSO {
|
||
public class AuthManager {
|
||
private OidcClient _oidcClient;
|
||
private readonly AuthConfig authConfig = new();
|
||
public static readonly AuthManager Instance = new AuthManager();
|
||
private AuthStatus authStatus = AuthStatus.Unknown;
|
||
|
||
//登录成功回调
|
||
public Action<TokenSet>? OnLoginSuccess;
|
||
//登录失败回调
|
||
public Action<string>? OnLoginFailed;
|
||
//刷新令牌成功回调
|
||
public Action<TokenSet>? OnRefreshTokenSuccess;
|
||
//刷新令牌失败回调
|
||
public Action<string>? OnRefreshTokenFailed;
|
||
//获取用户信息成功回调
|
||
public Action<UserInfoSet>? OnGetUserInfoSuccess;
|
||
//获取用户信息失败回调
|
||
public Action<string>? OnGetUserInfoFailed;
|
||
//撤销令牌成功回调
|
||
public Action? OnRevokeTokenSuccess;
|
||
//撤销令牌失败回调
|
||
public Action<string>? OnRevokeTokenFailed;
|
||
//注销登录成功回调
|
||
public Action? OnLogoutSuccess;
|
||
//注销登录失败回调
|
||
public Action<string>? OnLogoutFailed;
|
||
//登录状态改变回调
|
||
public Action<AuthStatus> OnAuthStatusChanged;
|
||
|
||
public AuthStatus AuthStatus {
|
||
get => authStatus; set {
|
||
authStatus = value;
|
||
OnAuthStatusChanged?.Invoke(value);
|
||
}
|
||
}
|
||
|
||
public AuthManager() {
|
||
var options = new OidcClientOptions {
|
||
Authority = authConfig.authorityUrl,
|
||
ClientId = authConfig.clientId,
|
||
RedirectUri = authConfig.redirectUri.ToString(),
|
||
Scope = authConfig.scope,
|
||
Policy = new Policy {
|
||
RequireAccessTokenHash = false,
|
||
},
|
||
|
||
LoadProfile = true
|
||
};
|
||
|
||
_oidcClient = new OidcClient(options);
|
||
Console.WriteLine("OIDC 客户端初始化");
|
||
}
|
||
|
||
public async void Login(Form? from = null) {
|
||
if (from == null) {
|
||
OnLoginFailed?.Invoke("登录窗口未初始化");
|
||
return;
|
||
}
|
||
if (_oidcClient == null) {
|
||
OnLoginFailed?.Invoke("OIDC客户端未初始化");
|
||
return;
|
||
}
|
||
if (AuthStatus == AuthStatus.Success) {
|
||
OnLoginFailed?.Invoke("已登录,请先注销");
|
||
return;
|
||
}
|
||
|
||
AuthStatus = AuthStatus.InProgress;
|
||
_oidcClient.Options.Browser = new WebView2Browser(from);
|
||
|
||
try {
|
||
|
||
var result = await _oidcClient.LoginAsync(new LoginRequest());
|
||
if (result.IsError) {
|
||
AuthStatus = AuthStatus.Failed;
|
||
OnLoginFailed?.Invoke(result.Error);
|
||
} else {
|
||
TokenSet tokenSet = new(
|
||
result.AccessToken,
|
||
result.RefreshToken,
|
||
result.IdentityToken
|
||
);
|
||
SsoException? exception = CheckToken(tokenSet);
|
||
if (exception != null) {
|
||
throw exception;
|
||
}
|
||
AuthStatus = AuthStatus.Success;
|
||
OnLoginSuccess?.Invoke(tokenSet);
|
||
|
||
}
|
||
} catch (Exception ex) {
|
||
AuthStatus = AuthStatus.Failed;
|
||
OnLoginFailed?.Invoke("登录异常:" + ex.Message);
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 获取用户信息
|
||
/// </summary>
|
||
/// <param name="accessToken">访问令牌</param>
|
||
/// <returns>用户信息</returns>
|
||
public UserInfoSet? GetUserInfo(string? accessToken) {
|
||
try {
|
||
if (string.IsNullOrWhiteSpace(accessToken)) {
|
||
throw new SsoException("AccessToken 为空");
|
||
}
|
||
|
||
using var httpClient = new HttpClient();
|
||
var request = new HttpRequestMessage(HttpMethod.Get, authConfig.userInfoEndpoint);
|
||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||
|
||
var response = httpClient.Send(request);
|
||
if (!response.IsSuccessStatusCode) {
|
||
OnGetUserInfoFailed?.Invoke($"获取用户信息失败,状态码: {response.StatusCode}");
|
||
return null;
|
||
}
|
||
|
||
var content = response.Content.ReadAsStringAsync().Result;
|
||
var userInfoSet = JsonSerializer.Deserialize<UserInfoSet>(content);
|
||
if (userInfoSet == null) {
|
||
throw new SsoException("获取用户信息失败,内容解析失败");
|
||
}
|
||
OnGetUserInfoSuccess?.Invoke(userInfoSet);
|
||
return userInfoSet;
|
||
} catch (Exception ex) {
|
||
OnGetUserInfoFailed?.Invoke($"获取用户信息异常: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 刷新令牌
|
||
/// </summary>
|
||
/// <param name="refreshToken">刷新令牌</param>
|
||
/// <returns>令牌信息</returns>
|
||
public async Task<TokenSet?> RefreshToken(string? refreshToken) {
|
||
try {
|
||
if (_oidcClient == null) {
|
||
OnRefreshTokenFailed?.Invoke("OidcClient 未初始化");
|
||
return null;
|
||
}
|
||
if (string.IsNullOrWhiteSpace(refreshToken)) {
|
||
OnRefreshTokenFailed?.Invoke("刷新令牌失败:refreshToken 不能为空");
|
||
return null;
|
||
}
|
||
// 调用 OidcClient 的刷新令牌方法
|
||
RefreshTokenResult result = await _oidcClient.RefreshTokenAsync(refreshToken);
|
||
|
||
if (result.IsError) {
|
||
OnRefreshTokenFailed?.Invoke($"刷新令牌失败: {result.Error}");
|
||
return null;
|
||
}
|
||
var tokenSet = new TokenSet(
|
||
result.AccessToken,
|
||
result.RefreshToken,
|
||
result.IdentityToken
|
||
);
|
||
SsoException? exception = CheckToken(tokenSet);
|
||
if (exception != null) {
|
||
throw exception;
|
||
}
|
||
AuthStatus = AuthStatus.Success;
|
||
OnRefreshTokenSuccess?.Invoke(tokenSet);
|
||
return tokenSet;
|
||
} catch (Exception ex) {
|
||
OnRefreshTokenFailed?.Invoke($"刷新令牌异常: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 撤销令牌
|
||
/// </summary>
|
||
/// <param name="token">token</param>
|
||
/// <param name="tokenTypeHint">token id</param>
|
||
public async void RevokeToken(string? token, string? tokenTypeHint, bool form = false) {
|
||
if (string.IsNullOrWhiteSpace(token)) {
|
||
OnRevokeTokenFailed?.Invoke("撤销令牌失败:token 不能为空");
|
||
if (form) {
|
||
OnLogoutFailed?.Invoke("token 不能为空");
|
||
}
|
||
return;
|
||
}
|
||
if (string.IsNullOrWhiteSpace(tokenTypeHint)) {
|
||
OnRevokeTokenFailed?.Invoke("撤销令牌失败:tokenTypeHint 不能为空");
|
||
if (form) {
|
||
OnLogoutFailed?.Invoke("tokenTypeHint 不能为空");
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
using var httpClient = new HttpClient();
|
||
var parameters = new Dictionary<string, string>
|
||
{
|
||
{ "token", token }
|
||
};
|
||
if (!string.IsNullOrWhiteSpace(tokenTypeHint)) {
|
||
parameters.Add("token_type_hint", tokenTypeHint);
|
||
}
|
||
parameters.Add("client_id", authConfig.clientId);
|
||
|
||
// 如果有 client_secret,说明是 confidential client
|
||
var clientSecretProperty = authConfig.GetType().GetProperty("clientSecret");
|
||
if (clientSecretProperty != null) {
|
||
var clientSecret = clientSecretProperty.GetValue(authConfig) as string;
|
||
if (!string.IsNullOrWhiteSpace(clientSecret)) {
|
||
parameters.Add("client_secret", clientSecret);
|
||
}
|
||
}
|
||
|
||
var content = new FormUrlEncodedContent(parameters);
|
||
var response = await httpClient.PostAsync(authConfig.revocationEndpoint, content);
|
||
var respContent = await response.Content.ReadAsStringAsync();
|
||
|
||
if (response.IsSuccessStatusCode) {
|
||
// 撤销令牌成功,修改登录状态
|
||
AuthStatus = AuthStatus.LoginOut;
|
||
OnRevokeTokenSuccess?.Invoke();
|
||
if (form) {
|
||
OnLogoutSuccess?.Invoke();
|
||
}
|
||
return;
|
||
} else {
|
||
OnRevokeTokenFailed?.Invoke($"撤销令牌失败,状态码: {response.StatusCode},内容: {respContent}");
|
||
if (form) {
|
||
OnLogoutFailed?.Invoke($"撤销令牌失败,状态码: {response.StatusCode},内容: {respContent}");
|
||
}
|
||
return;
|
||
}
|
||
} catch (Exception ex) {
|
||
OnRevokeTokenFailed?.Invoke($"撤销令牌异常: {ex.Message}");
|
||
if (form) {
|
||
OnLogoutFailed?.Invoke($"撤销令牌异常: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 注销登录
|
||
/// </summary>
|
||
public void Logout(TokenSet? set) {
|
||
SsoException? exception = CheckToken(set);
|
||
if (exception != null) {
|
||
OnLogoutFailed?.Invoke(exception.Message);
|
||
return;
|
||
}
|
||
|
||
#pragma warning disable CS8602 // 解引用可能出现空引用。
|
||
RevokeToken(set.RefreshToken, set.IdToken, true);
|
||
#pragma warning restore CS8602 // 解引用可能出现空引用。
|
||
//TokenManager
|
||
}
|
||
/// <summary>
|
||
/// 检查 TokenSet 是否有效
|
||
/// </summary>
|
||
/// <param name="tokenSet">TokenSet 对象</param>
|
||
/// <returns>如果有效返回Null,如果无效返回对应的Exception</returns>
|
||
public static SsoException? CheckToken(TokenSet? tokenSet) {
|
||
if (tokenSet == null) {
|
||
return new SsoException("TokenSet 为空");
|
||
}
|
||
if (string.IsNullOrWhiteSpace(tokenSet.AccessToken)) {
|
||
return new SsoException("AccessToken 为空");
|
||
}
|
||
if (string.IsNullOrWhiteSpace(tokenSet.RefreshToken)) {
|
||
return new SsoException("RefreshToken 为空");
|
||
}
|
||
if (string.IsNullOrWhiteSpace(tokenSet.IdToken)) {
|
||
return new SsoException("IdToken 为空");
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
}
|