添加项目文件。
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
using Avalonia_Services.Services.AuthService;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Avalonia_PC.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// 第三方授权客户端占位实现。接入真实第三方接口时替换此服务即可。
|
||||
/// </summary>
|
||||
public sealed class DefaultPcThirdPartyAuthorizationClient : IPcThirdPartyAuthorizationClient
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证第三方授权码是否有效。默认实现将 "invalid" 视为授权丢失,其余视为有效。
|
||||
/// </summary>
|
||||
/// <param name="authorizationCode">第三方授权码。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>授权检查结果。</returns>
|
||||
public Task<ThirdPartyAuthCheckResult> ValidateAuthorizationCodeAsync(
|
||||
string authorizationCode,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(authorizationCode) ||
|
||||
string.Equals(authorizationCode, "invalid", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(ThirdPartyAuthCheckResult.AuthorizationLost);
|
||||
}
|
||||
|
||||
return Task.FromResult(ThirdPartyAuthCheckResult.Valid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新第三方授权。默认实现总是返回 TemporaryFailure,表示暂时无法刷新。
|
||||
/// </summary>
|
||||
/// <param name="authorizationReference">授权引用标识。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>授权检查结果。</returns>
|
||||
public Task<ThirdPartyAuthCheckResult> RefreshAuthorizationAsync(
|
||||
string authorizationReference,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.Equals(authorizationReference, "invalid", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(ThirdPartyAuthCheckResult.AuthorizationLost);
|
||||
}
|
||||
|
||||
return Task.FromResult(ThirdPartyAuthCheckResult.TemporaryFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using Authentication;
|
||||
using Avalonia_Common.Core;
|
||||
using Avalonia_Services.Core;
|
||||
using Avalonia_Services.Services.AuthService;
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Avalonia_PC.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// PC 端鉴权端点服务,实现 <see cref="IPcAuthEndpointService"/>,
|
||||
/// 处理授权码登录、Token 刷新和登出操作。
|
||||
/// </summary>
|
||||
public sealed class PcAuthEndpointService(PcGlobalTokenService tokenService) : IPcAuthEndpointService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<object?> AuthorizeAsync(ServiceEndpointContext ctx)
|
||||
{
|
||||
var request = Deserialize<PcAuthorizeRequest>(ctx.Body);
|
||||
var token = await tokenService.AuthorizeAsync(request?.AuthorizationCode);
|
||||
if (token is null)
|
||||
{
|
||||
ctx.StatusCode = 401;
|
||||
return ResponseHelper.Failure(401, "授权失败");
|
||||
}
|
||||
|
||||
return ResponseHelper.Ok(token, "授权成功");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<object?> RefreshAsync(ServiceEndpointContext ctx)
|
||||
{
|
||||
var request = Deserialize<PcRefreshRequest>(ctx.Body);
|
||||
var token = request?.Token ?? ExtractBearerToken(ctx.GetHeader("Authorization"));
|
||||
var refreshed = await tokenService.RefreshAsync(token);
|
||||
if (refreshed is null)
|
||||
{
|
||||
ctx.StatusCode = 401;
|
||||
return ResponseHelper.Failure(401, "授权已失效");
|
||||
}
|
||||
|
||||
return ResponseHelper.Ok(refreshed, "刷新成功");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<object?> LogoutAsync(ServiceEndpointContext ctx)
|
||||
{
|
||||
var request = Deserialize<PcLogoutRequest>(ctx.Body);
|
||||
var token = request?.Token ?? ExtractBearerToken(ctx.GetHeader("Authorization"));
|
||||
tokenService.Logout(token);
|
||||
return Task.FromResult<object?>(ResponseHelper.Succeed("退出成功"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 JSON 请求体反序列化为指定类型。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标类型。</typeparam>
|
||||
/// <param name="body">JSON 请求体字符串,可为空。</param>
|
||||
/// <returns>反序列化后的对象;若 body 为空则返回默认值。</returns>
|
||||
private static T? Deserialize<T>(string? body)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(body)
|
||||
? default
|
||||
: JsonSerializer.Deserialize<T>(body, JsonOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Authorization 头中提取 Bearer Token。
|
||||
/// </summary>
|
||||
/// <param name="authorization">Authorization 头的值。</param>
|
||||
/// <returns>提取的 Token 字符串;若无法提取则返回 null。</returns>
|
||||
private static string? ExtractBearerToken(string? authorization)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(authorization))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bearer Token 的前缀常量。
|
||||
/// </summary>
|
||||
const string prefix = "Bearer ";
|
||||
return authorization.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
||||
? authorization[prefix.Length..].Trim()
|
||||
: authorization.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Authentication;
|
||||
using Avalonia_Services.Core;
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Avalonia_PC.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// PC 端鉴权服务,基于全局 Token 验证用户身份,实现 <see cref="IAuthService"/>。
|
||||
/// </summary>
|
||||
public sealed class PcAuthService(PcGlobalTokenService tokenService) : IAuthService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<ClaimsPrincipal?> AuthenticateAsync(ServiceEndpointContext context)
|
||||
{
|
||||
var token = ExtractBearerToken(context.GetHeader("Authorization"));
|
||||
if (!await tokenService.ValidateAsync(token))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var identity = new ClaimsIdentity(
|
||||
[
|
||||
new Claim(ClaimTypes.NameIdentifier, "pc-local"),
|
||||
new Claim(ClaimTypes.Name, "PC授权用户"),
|
||||
new Claim(ClaimTypes.Role, "SuperAdmin"),
|
||||
new Claim(ClaimTypes.Role, "Admin"),
|
||||
new Claim("auth_type", "pc-global-token"),
|
||||
],
|
||||
"pc-global-token");
|
||||
|
||||
return new ClaimsPrincipal(identity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> AuthorizeAsync(ClaimsPrincipal user, string policy)
|
||||
{
|
||||
return Task.FromResult(user.Identity?.IsAuthenticated == true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Authorization 头中提取 Bearer Token。
|
||||
/// </summary>
|
||||
/// <param name="authorization">Authorization 头的值。</param>
|
||||
/// <returns>提取的 Token 字符串;若无法提取则返回 null。</returns>
|
||||
private static string? ExtractBearerToken(string? authorization)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(authorization))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string prefix = "Bearer ";
|
||||
return authorization.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
||||
? authorization[prefix.Length..].Trim()
|
||||
: authorization.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user