feat: initialize Evergreen Health backend

This commit is contained in:
fuenmao
2026-09-21 14:08:22 +08:00
commit 8553df5dd2
493 changed files with 75649 additions and 0 deletions
@@ -0,0 +1,27 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Microsoft.AspNetCore.Authentication;
namespace Admin.NET.Core;
/// <summary>
/// 获取 AccessKey 关联 AccessSecret 方法的上下文
/// </summary>
public class GetAccessSecretContext : BaseContext<SignatureAuthenticationOptions>
{
public GetAccessSecretContext(HttpContext context,
AuthenticationScheme scheme,
SignatureAuthenticationOptions options)
: base(context, scheme, options)
{
}
/// <summary>
/// 身份标识
/// </summary>
public string AccessKey { get; set; }
}
@@ -0,0 +1,23 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// Signature 身份验证处理程序相关的默认值
/// </summary>
public static class SignatureAuthenticationDefaults
{
/// <summary>
/// SignatureAuthenticationOptions.AuthenticationScheme 使用的默认值
/// </summary>
public const string AuthenticationScheme = "Signature";
/// <summary>
/// 附加在 HttpContext Item 中验证失败消息的 Key
/// </summary>
public const string AuthenticateFailMsgKey = "SignatureAuthenticateFailMsg";
}
@@ -0,0 +1,53 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// Signature 身份验证事件
/// </summary>
public class SignatureAuthenticationEvent
{
public SignatureAuthenticationEvent()
{
}
/// <summary>
/// 获取或设置获取 AccessKey 的 AccessSecret 的逻辑处理
/// </summary>
public Func<GetAccessSecretContext, Task<string>> OnGetAccessSecret { get; set; }
/// <summary>
/// 获取或设置质询的逻辑处理
/// </summary>
public Func<SignatureChallengeContext, Task> OnChallenge { get; set; } = _ => Task.CompletedTask;
/// <summary>
/// 获取或设置已验证的逻辑处理
/// </summary>
public Func<SignatureValidatedContext, Task> OnValidated { get; set; } = _ => Task.CompletedTask;
/// <summary>
/// 获取 AccessKey 的 AccessSecret
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public virtual Task<string> GetAccessSecret(GetAccessSecretContext context) => OnGetAccessSecret?.Invoke(context) ?? throw new NotImplementedException($"需要提供 {nameof(OnGetAccessSecret)} 实现");
/// <summary>
/// 质询
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public virtual Task Challenge(SignatureChallengeContext context) => OnChallenge?.Invoke(context) ?? Task.CompletedTask;
/// <summary>
/// 已验证成功
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public virtual Task Validated(SignatureValidatedContext context) => OnValidated?.Invoke(context) ?? Task.CompletedTask;
}
@@ -0,0 +1,36 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Microsoft.AspNetCore.Authentication;
namespace Admin.NET.Core;
/// <summary>
/// Signature 身份验证扩展
/// </summary>
public static class SignatureAuthenticationExtensions
{
/// <summary>
/// 注册 Signature 身份验证处理模块
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static AuthenticationBuilder AddSignatureAuthentication(this AuthenticationBuilder builder)
{
return builder.AddSignatureAuthentication(options => { });
}
/// <summary>
/// 注册 Signature 身份验证处理模块
/// </summary>
/// <param name="builder"></param>
/// <param name="options"></param>
/// <returns></returns>
public static AuthenticationBuilder AddSignatureAuthentication(this AuthenticationBuilder builder, Action<SignatureAuthenticationOptions> options)
{
return builder.AddScheme<SignatureAuthenticationOptions, SignatureAuthenticationHandler>(SignatureAuthenticationDefaults.AuthenticationScheme, options);
}
}
@@ -0,0 +1,177 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Microsoft.AspNetCore.Authentication;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text.Encodings.Web;
namespace Admin.NET.Core;
/// <summary>
/// Signature 身份验证处理
/// </summary>
public sealed class SignatureAuthenticationHandler : AuthenticationHandler<SignatureAuthenticationOptions>
{
#if NET6_0
public SignatureAuthenticationHandler(IOptionsMonitor<SignatureAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
#else
public SignatureAuthenticationHandler(IOptionsMonitor<SignatureAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder)
{
}
#endif
private readonly SysCacheService _sysCacheService = App.GetRequiredService<SysCacheService>();
private new SignatureAuthenticationEvent Events
{
get => (SignatureAuthenticationEvent)base.Events;
set => base.Events = value;
}
/// <summary>
/// 确保创建的 Event 类型是 DigestEvents
/// </summary>
/// <returns></returns>
protected override Task<object> CreateEventsAsync() => throw new NotImplementedException($"{nameof(SignatureAuthenticationOptions)}.{nameof(SignatureAuthenticationOptions.Events)} 需要提供一个实例");
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var accessKey = Request.Headers["accessKey"].FirstOrDefault();
var timestampStr = Request.Headers["timestamp"].FirstOrDefault(); // 精确到秒
var nonce = Request.Headers["nonce"].FirstOrDefault();
var sign = Request.Headers["sign"].FirstOrDefault();
if (string.IsNullOrEmpty(accessKey))
return await AuthenticateResultFailAsync("accessKey 不能为空");
if (string.IsNullOrEmpty(timestampStr))
return await AuthenticateResultFailAsync("timestamp 不能为空");
if (string.IsNullOrEmpty(nonce))
return await AuthenticateResultFailAsync("nonce 不能为空");
if (string.IsNullOrEmpty(sign))
return await AuthenticateResultFailAsync("sign 不能为空");
// 验证请求数据是否在可接受的时间内
if (!long.TryParse(timestampStr, out var timestamp))
return await AuthenticateResultFailAsync("timestamp 值不合法");
var requestDate = DateTimeUtil.ConvertUnixTime(timestamp);
#if NET6_0
var utcNow = Clock.UtcNow;
#else
var utcNow = TimeProvider.GetUtcNow();
#endif
if (requestDate > utcNow.Add(Options.AllowedDateDrift).LocalDateTime || requestDate < utcNow.Subtract(Options.AllowedDateDrift).LocalDateTime)
return await AuthenticateResultFailAsync("timestamp 值已超过允许的偏差范围");
// 获取 accessSecret
var getAccessSecretContext = new GetAccessSecretContext(Context, Scheme, Options) { AccessKey = accessKey };
var accessSecret = await Events.GetAccessSecret(getAccessSecretContext);
if (string.IsNullOrEmpty(accessSecret))
return await AuthenticateResultFailAsync("accessKey 无效");
// 校验签名
var appSecretByte = Encoding.UTF8.GetBytes(accessSecret);
string serverSign = SignData(appSecretByte, GetMessageForSign(Context));
if (serverSign != sign)
return await AuthenticateResultFailAsync("sign 无效的签名");
// 重放检测
var cacheKey = $"{CacheConst.KeyOpenAccessNonce}{accessKey}|{nonce}";
if (_sysCacheService.ExistKey(cacheKey)) return await AuthenticateResultFailAsync("重复的请求");
_sysCacheService.Set(cacheKey, null, Options.AllowedDateDrift * 2); // 缓存过期时间为偏差范围时间的2倍
// 已验证成功
var signatureValidatedContext = new SignatureValidatedContext(Context, Scheme, Options)
{
Principal = new ClaimsPrincipal(new ClaimsIdentity(SignatureAuthenticationDefaults.AuthenticationScheme)),
AccessKey = accessKey
};
await Events.Validated(signatureValidatedContext);
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (signatureValidatedContext.Result != null)
return signatureValidatedContext.Result;
// ReSharper disable once HeuristicUnreachableCode
signatureValidatedContext.Success();
return signatureValidatedContext.Result;
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
var authResult = await HandleAuthenticateOnceSafeAsync();
var challengeContext = new SignatureChallengeContext(Context, Scheme, Options, properties)
{
AuthenticateFailure = authResult.Failure,
};
await Events.Challenge(challengeContext);
// 质询已处理
if (challengeContext.Handled) return;
await base.HandleChallengeAsync(properties);
}
/// <summary>
/// 获取用于签名的消息
/// </summary>
/// <returns></returns>
private static string GetMessageForSign(HttpContext context)
{
var method = context.Request.Method; // 请求方法(大写)
var url = context.Request.Path; // 请求 url,去除协议、域名、参数,以 / 开头
var accessKey = context.Request.Headers["accessKey"].FirstOrDefault(); // 身份标识
var timestamp = context.Request.Headers["timestamp"].FirstOrDefault(); // 时间戳,精确到秒
var nonce = context.Request.Headers["nonce"].FirstOrDefault(); // 唯一随机数
return $"{method}&{url}&{accessKey}&{timestamp}&{nonce}";
}
/// <summary>
/// 对数据进行签名
/// </summary>
/// <param name="secret"></param>
/// <param name="data"></param>
/// <returns></returns>
private static string SignData(byte[] secret, string data)
{
if (secret == null)
throw new ArgumentNullException(nameof(secret));
if (data == null)
throw new ArgumentNullException(nameof(data));
using HMAC hmac = new HMACSHA256();
hmac.Key = secret;
return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(data)));
}
/// <summary>
/// 返回验证失败结果,并在 Items 中增加 <see cref="SignatureAuthenticationDefaults.AuthenticateFailMsgKey"/>,记录身份验证失败消息
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
private Task<AuthenticateResult> AuthenticateResultFailAsync(string message)
{
// 写入身份验证失败消息
Context.Items[SignatureAuthenticationDefaults.AuthenticateFailMsgKey] = message;
return Task.FromResult(AuthenticateResult.Fail(message));
}
}
@@ -0,0 +1,29 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Microsoft.AspNetCore.Authentication;
namespace Admin.NET.Core;
/// <summary>
/// Signature 身份验证选项
/// </summary>
public class SignatureAuthenticationOptions : AuthenticationSchemeOptions
{
/// <summary>
/// 请求时间允许的偏差范围
/// </summary>
public TimeSpan AllowedDateDrift { get; set; } = TimeSpan.FromMinutes(5);
/// <summary>
/// Signature 身份验证事件
/// </summary>
public new SignatureAuthenticationEvent Events
{
get => (SignatureAuthenticationEvent)base.Events;
set => base.Events = value;
}
}
@@ -0,0 +1,33 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Microsoft.AspNetCore.Authentication;
namespace Admin.NET.Core;
/// <summary>
/// Signature 身份验证质询上下文
/// </summary>
public class SignatureChallengeContext : PropertiesContext<SignatureAuthenticationOptions>
{
public SignatureChallengeContext(HttpContext context,
AuthenticationScheme scheme,
SignatureAuthenticationOptions options,
AuthenticationProperties properties)
: base(context, scheme, options, properties)
{
}
/// <summary>
/// 在认证期间出现的异常
/// </summary>
public Exception AuthenticateFailure { get; set; }
/// <summary>
/// 指定是否已被处理,如果已处理,则跳过默认认证逻辑
/// </summary>
public bool Handled { get; private set; }
}
@@ -0,0 +1,32 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Microsoft.AspNetCore.Authentication;
namespace Admin.NET.Core;
/// <summary>
/// Signature 身份验证已验证上下文
/// </summary>
public class SignatureValidatedContext : ResultContext<SignatureAuthenticationOptions>
{
public SignatureValidatedContext(HttpContext context,
AuthenticationScheme scheme,
SignatureAuthenticationOptions options)
: base(context, scheme, options)
{
}
/// <summary>
/// 身份标识
/// </summary>
public string AccessKey { get; set; }
/// <summary>
/// 密钥
/// </summary>
public string AccessSecret { get; set; }
}