using System.Security.Cryptography;
using System.Text;
using Fake.Application;
using Fake.DependencyInjection;
using Microsoft.AspNetCore.Http;
using Oms.Domain.CustomersAggregate.Entities;
using Oms.Domain.CustomersAggregate.Enums;
using Oms.Domain.CustomersAggregate.Repositories;
namespace Oms.Application.Shared;
///
/// 客户 OpenAPI 机机鉴权:appKey + timestamp + HMAC-SHA256(sign),Secret 仅本地参与签名。
///
public class CustomerOpenApiAuthHelper : IScopedDependency
{
public const string AppKeyHeaderName = "appKey";
public const string TimestampHeaderName = "timestamp";
public const string SignHeaderName = "sign";
/// 签名时间窗(秒)。
public const int TimestampSkewSeconds = 300;
private readonly ICustomerOpenApiCredentialRepository _credentialRepository;
private readonly ICustomerRepository _customerRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
public CustomerOpenApiAuthHelper(
ICustomerOpenApiCredentialRepository credentialRepository,
ICustomerRepository customerRepository,
IHttpContextAccessor httpContextAccessor)
{
_credentialRepository = credentialRepository;
_customerRepository = customerRepository;
_httpContextAccessor = httpContextAccessor;
}
///
/// 校验 Header 签名并解析主客户。
/// stringToSign = appKey + "\n" + timestamp + "\n" + sha256Hex(rawBody)
/// sign = hex(HMAC-SHA256(appSecret, stringToSign))
///
public async Task<(Customer Customer, CustomerOpenApiCredential Credential)> ResolveSignedAsync(
string? appKey,
string? timestamp,
string? sign,
string? rawBody,
CancellationToken cancellationToken = default)
{
var key = appKey?.Trim();
var tsRaw = timestamp?.Trim();
var signRaw = sign?.Trim();
if (string.IsNullOrWhiteSpace(key)
|| string.IsNullOrWhiteSpace(tsRaw)
|| string.IsNullOrWhiteSpace(signRaw))
{
throw new BusinessException("凭证无效");
}
if (!long.TryParse(tsRaw, out var unixSeconds))
{
throw new BusinessException("请求已过期");
}
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (Math.Abs(now - unixSeconds) > TimestampSkewSeconds)
{
throw new BusinessException("请求已过期");
}
var credential = await _credentialRepository.GetByAppKeyAsync(key, cancellationToken);
if (credential == null
|| !credential.Enabled
|| string.IsNullOrWhiteSpace(credential.AppSecret))
{
throw new BusinessException("凭证无效");
}
var expectedSign = ComputeSign(key, tsRaw, rawBody ?? string.Empty, credential.AppSecret);
if (!FixedTimeEqualsUtf8(expectedSign, signRaw.ToLowerInvariant()))
{
throw new BusinessException("签名无效");
}
var customer = await _customerRepository.FirstOrDefaultAsync(
x => x.Id == credential.CustomerId,
cancellationToken);
if (customer == null
|| customer.ParentCustomerId.HasValue
|| customer.Status != CustomerStatus.Normal)
{
throw new BusinessException("凭证无效");
}
credential.LastUsedAt = DateTime.Now;
credential.LastUsedIp = ResolveClientIp();
await _credentialRepository.UpdateAsync(credential);
return (customer, credential);
}
///
/// 客户端可复用:计算 sign(小写 hex)。
///
public static string ComputeSign(string appKey, string timestamp, string rawBody, string appSecret)
{
var bodyHash = Sha256Hex(rawBody ?? string.Empty);
var stringToSign = $"{appKey}\n{timestamp}\n{bodyHash}";
return HmacSha256Hex(appSecret, stringToSign);
}
private static string Sha256Hex(string text)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(text));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
private static string HmacSha256Hex(string secret, string content)
{
var keyBytes = Encoding.UTF8.GetBytes(secret);
var contentBytes = Encoding.UTF8.GetBytes(content);
var hash = HMACSHA256.HashData(keyBytes, contentBytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static bool FixedTimeEqualsUtf8(string? expected, string actual)
{
if (string.IsNullOrEmpty(expected))
{
return false;
}
var left = Encoding.UTF8.GetBytes(expected);
var right = Encoding.UTF8.GetBytes(actual);
return left.Length == right.Length
&& CryptographicOperations.FixedTimeEquals(left, right);
}
private string? ResolveClientIp()
{
var http = _httpContextAccessor.HttpContext;
if (http == null)
{
return null;
}
var forwarded = http.Request.Headers["X-Forwarded-For"].FirstOrDefault();
if (!string.IsNullOrWhiteSpace(forwarded))
{
return forwarded.Split(',')[0].Trim();
}
return http.Connection.RemoteIpAddress?.ToString();
}
}