228 lines
8.8 KiB
C#
228 lines
8.8 KiB
C#
using Avalonia_Services.Services.AuthService;
|
||
using System;
|
||
using System.Security.Cryptography;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace Authentication
|
||
{
|
||
/// <summary>
|
||
/// PC 端全局 Token 服务,管理全局唯一的访问 Token。
|
||
/// 支持授权码登录、Token 刷新、有效性验证和登出,
|
||
/// 在第三方授权暂时失败时使用缩短有效期的临时 Token。
|
||
/// </summary>
|
||
public sealed class PcGlobalTokenService(IPcThirdPartyAuthorizationClient thirdPartyClient)
|
||
{
|
||
/// <summary>
|
||
/// 超级管理员角色集合。
|
||
/// </summary>
|
||
private static readonly string[] SuperRoles = ["SuperAdmin", "Admin"];
|
||
/// <summary>
|
||
/// 线程同步锁。
|
||
/// </summary>
|
||
private readonly object _syncRoot = new();
|
||
/// <summary>
|
||
/// 当前 Token 状态。
|
||
/// </summary>
|
||
private PcTokenState? _current;
|
||
|
||
/// <summary>
|
||
/// 正常 Token 有效期(8 小时)。
|
||
/// </summary>
|
||
private static readonly TimeSpan NormalLifetime = TimeSpan.FromHours(8);
|
||
/// <summary>
|
||
/// 第三方暂时失败时的 Token 有效期(20 分钟)。
|
||
/// </summary>
|
||
private static readonly TimeSpan TemporaryFailureLifetime = TimeSpan.FromMinutes(20);
|
||
/// <summary>
|
||
/// 第三方暂时失败的最长容忍窗口(24 小时),超出后清除 Token。
|
||
/// </summary>
|
||
private static readonly TimeSpan MaxTemporaryFailureWindow = TimeSpan.FromHours(24);
|
||
|
||
/// <summary>
|
||
/// 使用授权码进行登录授权,验证成功后颁发全局 Token。
|
||
/// </summary>
|
||
/// <param name="authorizationCode">第三方授权码。</param>
|
||
/// <param name="cancellationToken">取消令牌。</param>
|
||
/// <returns>Token 响应;若授权码无效则返回 null。</returns>
|
||
public async Task<PcTokenResponse?> AuthorizeAsync(string? authorizationCode, CancellationToken cancellationToken = default)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(authorizationCode))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var result = await thirdPartyClient.ValidateAuthorizationCodeAsync(authorizationCode, cancellationToken);
|
||
if (result != ThirdPartyAuthCheckResult.Valid)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return IssueToken(authorizationCode, NormalLifetime, resetTemporaryFailureWindow: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新当前 Token,向第三方验证授权引用是否仍然有效。
|
||
/// 根据第三方返回结果决定是续期、降级为临时 Token 还是清除。
|
||
/// </summary>
|
||
/// <param name="token">当前 Token。</param>
|
||
/// <param name="cancellationToken">取消令牌。</param>
|
||
/// <returns>新的 Token 响应;若授权丢失则返回 null。</returns>
|
||
public async Task<PcTokenResponse?> RefreshAsync(string? token, CancellationToken cancellationToken = default)
|
||
{
|
||
PcTokenState? current;
|
||
lock (_syncRoot)
|
||
{
|
||
current = IsCurrentToken(token) ? _current : null;
|
||
}
|
||
|
||
if (current is null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var result = await thirdPartyClient.RefreshAuthorizationAsync(current.AuthorizationReference, cancellationToken);
|
||
return result switch
|
||
{
|
||
ThirdPartyAuthCheckResult.Valid => IssueToken(current.AuthorizationReference, NormalLifetime, resetTemporaryFailureWindow: true),
|
||
ThirdPartyAuthCheckResult.AuthorizationLost => ClearAndReturnNull(),
|
||
ThirdPartyAuthCheckResult.TemporaryFailure => RefreshAfterTemporaryFailure(current),
|
||
_ => null,
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证 Token 是否有效,若已过期则尝试自动刷新。
|
||
/// </summary>
|
||
/// <param name="token">要验证的 Token。</param>
|
||
/// <param name="cancellationToken">取消令牌。</param>
|
||
/// <returns>Token 是否有效。</returns>
|
||
public async Task<bool> ValidateAsync(string? token, CancellationToken cancellationToken = default)
|
||
{
|
||
PcTokenState? current;
|
||
lock (_syncRoot)
|
||
{
|
||
if (!IsCurrentToken(token))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
current = _current;
|
||
if (current is not null && current.ExpiresAt > DateTime.UtcNow)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return await RefreshAsync(token, cancellationToken) is not null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 登出并清除当前 Token。
|
||
/// </summary>
|
||
/// <param name="token">要清除的 Token。</param>
|
||
public void Logout(string? token)
|
||
{
|
||
lock (_syncRoot)
|
||
{
|
||
if (IsCurrentToken(token))
|
||
{
|
||
_current = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 颁发新的全局 Token。
|
||
/// </summary>
|
||
/// <param name="authorizationReference">授权引用标识。</param>
|
||
/// <param name="lifetime">Token 有效期。</param>
|
||
/// <param name="resetTemporaryFailureWindow">是否重置暂时失败窗口。</param>
|
||
/// <returns>包含 Token 和过期时间的响应。</returns>
|
||
private PcTokenResponse IssueToken(string authorizationReference, TimeSpan lifetime, bool resetTemporaryFailureWindow)
|
||
{
|
||
var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
|
||
var now = DateTime.UtcNow;
|
||
var state = new PcTokenState(
|
||
HashToken(token),
|
||
authorizationReference,
|
||
now.Add(lifetime),
|
||
resetTemporaryFailureWindow ? null : _current?.TemporaryFailureStartedAt ?? now);
|
||
|
||
lock (_syncRoot)
|
||
{
|
||
_current = state;
|
||
}
|
||
|
||
return new PcTokenResponse(token, state.ExpiresAt, SuperRoles);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在第三方暂时失败时刷新 Token。若超出最大容忍窗口则清除 Token。
|
||
/// </summary>
|
||
/// <param name="current">当前 Token 状态。</param>
|
||
/// <returns>新的临时 Token 响应;若超出容忍窗口则返回 null。</returns>
|
||
private PcTokenResponse? RefreshAfterTemporaryFailure(PcTokenState current)
|
||
{
|
||
var startedAt = current.TemporaryFailureStartedAt ?? DateTime.UtcNow;
|
||
if (DateTime.UtcNow - startedAt > MaxTemporaryFailureWindow)
|
||
{
|
||
return ClearAndReturnNull();
|
||
}
|
||
|
||
return IssueToken(current.AuthorizationReference, TemporaryFailureLifetime, resetTemporaryFailureWindow: false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清除当前 Token 并返回 null。
|
||
/// </summary>
|
||
/// <returns>始终返回 null。</returns>
|
||
private PcTokenResponse? ClearAndReturnNull()
|
||
{
|
||
lock (_syncRoot)
|
||
{
|
||
_current = null;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查给定 Token 是否与当前持有的 Token 匹配。
|
||
/// </summary>
|
||
/// <param name="token">要检查的 Token。</param>
|
||
/// <returns>是否匹配。</returns>
|
||
private bool IsCurrentToken(string? token)
|
||
{
|
||
return !string.IsNullOrWhiteSpace(token) &&
|
||
_current is not null &&
|
||
string.Equals(_current.TokenHash, HashToken(token), StringComparison.Ordinal);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 对 Token 原文进行 SHA256 哈希,返回十六进制字符串。
|
||
/// </summary>
|
||
/// <param name="token">Token 原文。</param>
|
||
/// <returns>SHA256 哈希后的十六进制字符串。</returns>
|
||
private static string HashToken(string token)
|
||
{
|
||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
|
||
return Convert.ToHexString(bytes);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存当前全局 Token 的内部状态。
|
||
/// </summary>
|
||
/// <param name="TokenHash">Token 的 SHA256 哈希值。</param>
|
||
/// <param name="AuthorizationReference">授权引用标识。</param>
|
||
/// <param name="ExpiresAt">过期时间。</param>
|
||
/// <param name="TemporaryFailureStartedAt">第三方暂时失败的起始时间。</param>
|
||
private sealed record PcTokenState(
|
||
string TokenHash,
|
||
string AuthorizationReference,
|
||
DateTime ExpiresAt,
|
||
DateTime? TemporaryFailureStartedAt);
|
||
}
|
||
}
|