InboundSignatureHandler.cs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. using System.Security.Claims;
  2. using System.Security.Cryptography;
  3. using System.Text;
  4. using System.Text.Encodings.Web;
  5. using Admin.NET.Core;
  6. using Admin.NET.Core.Service;
  7. using Microsoft.AspNetCore.Authentication;
  8. using Microsoft.AspNetCore.Http;
  9. using Microsoft.Extensions.Logging;
  10. using Microsoft.Extensions.Options;
  11. namespace Admin.NET.Plugin.AiDOP.DataPlatform.Inbound;
  12. /// <summary>
  13. /// 插件侧入站验签:签名串含原始 body 摘要 + Idempotency-Key。不改框架 Core。
  14. /// </summary>
  15. public sealed class InboundSignatureHandler : AuthenticationHandler<InboundSignatureOptions>
  16. {
  17. private readonly SysOpenAccessService _openAccess;
  18. private readonly SysCacheService _cache;
  19. public InboundSignatureHandler(
  20. IOptionsMonitor<InboundSignatureOptions> options,
  21. ILoggerFactory logger,
  22. UrlEncoder encoder,
  23. SysOpenAccessService openAccess,
  24. SysCacheService cache)
  25. : base(options, logger, encoder)
  26. {
  27. _openAccess = openAccess;
  28. _cache = cache;
  29. }
  30. protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
  31. {
  32. var accessKey = Header(InboundSignatureDefaults.AccessKeyHeader);
  33. var timestampStr = Header(InboundSignatureDefaults.TimestampHeader);
  34. var nonce = Header(InboundSignatureDefaults.NonceHeader);
  35. var signature = Header(InboundSignatureDefaults.SignatureHeader);
  36. var idempotencyKey = Header(InboundSignatureDefaults.IdempotencyKeyHeader);
  37. var contentShaHeader = Header(InboundSignatureDefaults.ContentSha256Header);
  38. if (string.IsNullOrWhiteSpace(idempotencyKey))
  39. return Fail(InboundSignatureDefaults.MissingIdempotencyKey);
  40. if (string.IsNullOrWhiteSpace(accessKey))
  41. return Fail("missing X-Access-Key");
  42. if (string.IsNullOrWhiteSpace(timestampStr))
  43. return Fail("missing X-Timestamp");
  44. if (string.IsNullOrWhiteSpace(nonce))
  45. return Fail("missing X-Nonce");
  46. if (string.IsNullOrWhiteSpace(signature))
  47. return Fail("missing X-Signature");
  48. if (string.IsNullOrWhiteSpace(contentShaHeader))
  49. return Fail("missing X-Content-SHA256");
  50. if (!long.TryParse(timestampStr, out var unixSec))
  51. return Fail("invalid X-Timestamp");
  52. var nowUnix = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
  53. if (Math.Abs(nowUnix - unixSec) > Options.AllowedDateDrift.TotalSeconds)
  54. return Fail("timestamp outside allowed window");
  55. Request.EnableBuffering();
  56. using var ms = new MemoryStream();
  57. await Request.Body.CopyToAsync(ms);
  58. Request.Body.Position = 0;
  59. var bodyBytes = ms.ToArray();
  60. var contentSha256 = Convert.ToHexString(SHA256.HashData(bodyBytes)).ToLowerInvariant();
  61. if (!string.Equals(contentSha256, contentShaHeader.Trim(), StringComparison.OrdinalIgnoreCase))
  62. return Fail(InboundSignatureDefaults.ContentDigestMismatch);
  63. SysOpenAccess openAccess;
  64. try
  65. {
  66. openAccess = await _openAccess.GetByKey(accessKey);
  67. }
  68. catch (Exception ex)
  69. {
  70. Logger.LogWarning(ex, "InboundSignature GetByKey failed");
  71. return Fail("unknown AccessKey");
  72. }
  73. if (openAccess == null || string.IsNullOrWhiteSpace(openAccess.AccessSecret))
  74. return Fail("unknown AccessKey");
  75. var path = Request.Path.Value ?? string.Empty;
  76. // 接收 POST 与方案 §5.2 一致(POST&path&...);schema/receipts 等 GET 用实际方法名。
  77. var method = (Request.Method ?? "POST").Trim().ToUpperInvariant();
  78. var message = $"{method}&{path}&{accessKey}&{timestampStr}&{nonce}&{contentSha256}&{idempotencyKey}";
  79. var expected = Sign(openAccess.AccessSecret, message);
  80. var expectedBytes = Encoding.UTF8.GetBytes(expected);
  81. var actualBytes = Encoding.UTF8.GetBytes(signature);
  82. if (expectedBytes.Length != actualBytes.Length
  83. || !CryptographicOperations.FixedTimeEquals(expectedBytes, actualBytes))
  84. return Fail("invalid signature");
  85. var nonceKey = $"AIDOP_INBOUND_NONCE:{accessKey}:{nonce}";
  86. if (_cache.ExistKey(nonceKey))
  87. return Fail("nonce replay");
  88. _cache.Set(nonceKey, 1, Options.AllowedDateDrift * 2);
  89. var identity = new ClaimsIdentity(InboundSignatureDefaults.AuthenticationScheme);
  90. identity.AddClaim(new Claim(ClaimConst.TenantId, openAccess.BindTenantId.ToString()));
  91. identity.AddClaim(new Claim(ClaimConst.UserId, openAccess.BindUserId.ToString()));
  92. identity.AddClaim(new Claim(InboundSignatureDefaults.AccessKeyClaim, accessKey));
  93. var ticket = new AuthenticationTicket(
  94. new ClaimsPrincipal(identity),
  95. InboundSignatureDefaults.AuthenticationScheme);
  96. return AuthenticateResult.Success(ticket);
  97. }
  98. protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
  99. {
  100. var result = await HandleAuthenticateOnceSafeAsync();
  101. var message = result.Failure?.Message
  102. ?? Context.Items[InboundSignatureDefaults.FailMessageItemKey] as string
  103. ?? "unauthorized";
  104. var status = IsBadRequest(message) ? 400 : 401;
  105. if (Context.Response.HasStarted)
  106. return;
  107. Context.Response.StatusCode = status;
  108. Context.Response.ContentType = "application/json; charset=utf-8";
  109. await Context.Response.WriteAsync($"{{\"code\":{status},\"message\":{System.Text.Json.JsonSerializer.Serialize(message)},\"data\":null}}");
  110. }
  111. private AuthenticateResult Fail(string message)
  112. {
  113. Context.Items[InboundSignatureDefaults.FailMessageItemKey] = message;
  114. return AuthenticateResult.Fail(message);
  115. }
  116. private string Header(string name) => Request.Headers[name].FirstOrDefault();
  117. private static bool IsBadRequest(string message) =>
  118. string.Equals(message, InboundSignatureDefaults.MissingIdempotencyKey, StringComparison.OrdinalIgnoreCase)
  119. || string.Equals(message, InboundSignatureDefaults.ContentDigestMismatch, StringComparison.OrdinalIgnoreCase);
  120. private static string Sign(string secret, string message)
  121. {
  122. using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
  123. return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(message)));
  124. }
  125. }