- 新增 API 端 JWT 登录、refresh token 轮换和退出登录流程 - 新增 refresh token 实体、DbSet 配置和 EF Core 迁移 - 新增 PC 端授权码登录、本地全局 token 刷新、登出和鉴权服务 - 扩展统一端点模型,支持宿主过滤、角色鉴权、OpenAPI 元数据和 DI 服务处理器 - API 启用 JwtBearer 认证、Swagger UI 和认证端点注册 - PC 端注册认证服务,并按宿主过滤桌面拦截端点
149 lines
5.0 KiB
C#
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);
|
|
}
|
|
}
|