AvaloniaStack/Avalonia-PC/Authentication/PcGlobalTokenService.cs
luoqian a9abd90874 feat(auth): 添加统一 API 和 PC 认证端点
- 新增 API 端 JWT 登录、refresh token 轮换和退出登录流程
- 新增 refresh token 实体、DbSet 配置和 EF Core 迁移
- 新增 PC 端授权码登录、本地全局 token 刷新、登出和鉴权服务
- 扩展统一端点模型,支持宿主过滤、角色鉴权、OpenAPI 元数据和 DI 服务处理器
- API 启用 JwtBearer 认证、Swagger UI 和认证端点注册
- PC 端注册认证服务,并按宿主过滤桌面拦截端点
2026-05-15 17:35:07 +08:00

149 lines
5.0 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
{
public sealed class PcGlobalTokenService(IPcThirdPartyAuthorizationClient thirdPartyClient)
{
private static readonly string[] SuperRoles = ["SuperAdmin", "Admin"];
private readonly object _syncRoot = new();
private PcTokenState? _current;
private static readonly TimeSpan NormalLifetime = TimeSpan.FromHours(8);
private static readonly TimeSpan TemporaryFailureLifetime = TimeSpan.FromMinutes(20);
private static readonly TimeSpan MaxTemporaryFailureWindow = TimeSpan.FromHours(24);
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);
}
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,
};
}
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;
}
public void Logout(string? token)
{
lock (_syncRoot)
{
if (IsCurrentToken(token))
{
_current = null;
}
}
}
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);
}
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);
}
private PcTokenResponse? ClearAndReturnNull()
{
lock (_syncRoot)
{
_current = null;
}
return null;
}
private bool IsCurrentToken(string? token)
{
return !string.IsNullOrWhiteSpace(token) &&
_current is not null &&
string.Equals(_current.TokenHash, HashToken(token), StringComparison.Ordinal);
}
private static string HashToken(string token)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
return Convert.ToHexString(bytes);
}
private sealed record PcTokenState(
string TokenHash,
string AuthorizationReference,
DateTime ExpiresAt,
DateTime? TemporaryFailureStartedAt);
}
}