| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143 |
- using System.Security.Claims;
- using System.Security.Cryptography;
- using System.Text;
- using System.Text.Encodings.Web;
- using Admin.NET.Core;
- using Admin.NET.Core.Service;
- using Microsoft.AspNetCore.Authentication;
- using Microsoft.AspNetCore.Http;
- using Microsoft.Extensions.Logging;
- using Microsoft.Extensions.Options;
- namespace Admin.NET.Plugin.AiDOP.DataPlatform.Inbound;
- /// <summary>
- /// 插件侧入站验签:签名串含原始 body 摘要 + Idempotency-Key。不改框架 Core。
- /// </summary>
- public sealed class InboundSignatureHandler : AuthenticationHandler<InboundSignatureOptions>
- {
- private readonly SysOpenAccessService _openAccess;
- private readonly SysCacheService _cache;
- public InboundSignatureHandler(
- IOptionsMonitor<InboundSignatureOptions> options,
- ILoggerFactory logger,
- UrlEncoder encoder,
- SysOpenAccessService openAccess,
- SysCacheService cache)
- : base(options, logger, encoder)
- {
- _openAccess = openAccess;
- _cache = cache;
- }
- protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
- {
- var accessKey = Header(InboundSignatureDefaults.AccessKeyHeader);
- var timestampStr = Header(InboundSignatureDefaults.TimestampHeader);
- var nonce = Header(InboundSignatureDefaults.NonceHeader);
- var signature = Header(InboundSignatureDefaults.SignatureHeader);
- var idempotencyKey = Header(InboundSignatureDefaults.IdempotencyKeyHeader);
- var contentShaHeader = Header(InboundSignatureDefaults.ContentSha256Header);
- if (string.IsNullOrWhiteSpace(idempotencyKey))
- return Fail(InboundSignatureDefaults.MissingIdempotencyKey);
- if (string.IsNullOrWhiteSpace(accessKey))
- return Fail("missing X-Access-Key");
- if (string.IsNullOrWhiteSpace(timestampStr))
- return Fail("missing X-Timestamp");
- if (string.IsNullOrWhiteSpace(nonce))
- return Fail("missing X-Nonce");
- if (string.IsNullOrWhiteSpace(signature))
- return Fail("missing X-Signature");
- if (string.IsNullOrWhiteSpace(contentShaHeader))
- return Fail("missing X-Content-SHA256");
- if (!long.TryParse(timestampStr, out var unixSec))
- return Fail("invalid X-Timestamp");
- var nowUnix = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
- if (Math.Abs(nowUnix - unixSec) > Options.AllowedDateDrift.TotalSeconds)
- return Fail("timestamp outside allowed window");
- Request.EnableBuffering();
- using var ms = new MemoryStream();
- await Request.Body.CopyToAsync(ms);
- Request.Body.Position = 0;
- var bodyBytes = ms.ToArray();
- var contentSha256 = Convert.ToHexString(SHA256.HashData(bodyBytes)).ToLowerInvariant();
- if (!string.Equals(contentSha256, contentShaHeader.Trim(), StringComparison.OrdinalIgnoreCase))
- return Fail(InboundSignatureDefaults.ContentDigestMismatch);
- SysOpenAccess openAccess;
- try
- {
- openAccess = await _openAccess.GetByKey(accessKey);
- }
- catch (Exception ex)
- {
- Logger.LogWarning(ex, "InboundSignature GetByKey failed");
- return Fail("unknown AccessKey");
- }
- if (openAccess == null || string.IsNullOrWhiteSpace(openAccess.AccessSecret))
- return Fail("unknown AccessKey");
- var path = Request.Path.Value ?? string.Empty;
- // 接收 POST 与方案 §5.2 一致(POST&path&...);schema/receipts 等 GET 用实际方法名。
- var method = (Request.Method ?? "POST").Trim().ToUpperInvariant();
- var message = $"{method}&{path}&{accessKey}&{timestampStr}&{nonce}&{contentSha256}&{idempotencyKey}";
- var expected = Sign(openAccess.AccessSecret, message);
- var expectedBytes = Encoding.UTF8.GetBytes(expected);
- var actualBytes = Encoding.UTF8.GetBytes(signature);
- if (expectedBytes.Length != actualBytes.Length
- || !CryptographicOperations.FixedTimeEquals(expectedBytes, actualBytes))
- return Fail("invalid signature");
- var nonceKey = $"AIDOP_INBOUND_NONCE:{accessKey}:{nonce}";
- if (_cache.ExistKey(nonceKey))
- return Fail("nonce replay");
- _cache.Set(nonceKey, 1, Options.AllowedDateDrift * 2);
- var identity = new ClaimsIdentity(InboundSignatureDefaults.AuthenticationScheme);
- identity.AddClaim(new Claim(ClaimConst.TenantId, openAccess.BindTenantId.ToString()));
- identity.AddClaim(new Claim(ClaimConst.UserId, openAccess.BindUserId.ToString()));
- identity.AddClaim(new Claim(InboundSignatureDefaults.AccessKeyClaim, accessKey));
- var ticket = new AuthenticationTicket(
- new ClaimsPrincipal(identity),
- InboundSignatureDefaults.AuthenticationScheme);
- return AuthenticateResult.Success(ticket);
- }
- protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
- {
- var result = await HandleAuthenticateOnceSafeAsync();
- var message = result.Failure?.Message
- ?? Context.Items[InboundSignatureDefaults.FailMessageItemKey] as string
- ?? "unauthorized";
- var status = IsBadRequest(message) ? 400 : 401;
- if (Context.Response.HasStarted)
- return;
- Context.Response.StatusCode = status;
- Context.Response.ContentType = "application/json; charset=utf-8";
- await Context.Response.WriteAsync($"{{\"code\":{status},\"message\":{System.Text.Json.JsonSerializer.Serialize(message)},\"data\":null}}");
- }
- private AuthenticateResult Fail(string message)
- {
- Context.Items[InboundSignatureDefaults.FailMessageItemKey] = message;
- return AuthenticateResult.Fail(message);
- }
- private string Header(string name) => Request.Headers[name].FirstOrDefault();
- private static bool IsBadRequest(string message) =>
- string.Equals(message, InboundSignatureDefaults.MissingIdempotencyKey, StringComparison.OrdinalIgnoreCase)
- || string.Equals(message, InboundSignatureDefaults.ContentDigestMismatch, StringComparison.OrdinalIgnoreCase);
- private static string Sign(string secret, string message)
- {
- using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
- return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(message)));
- }
- }
|