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 + AppSecret 解析主客户。
///
public class CustomerOpenApiAuthHelper : IScopedDependency
{
public const string AppKeyHeaderName = "appKey";
public const string AppSecretHeaderName = "appSecret";
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;
}
///
/// 从当前请求头读取 appKey / appSecret 并鉴权。
///
public Task<(Customer Customer, CustomerOpenApiCredential Credential)> ResolveFromHeadersAsync(
CancellationToken cancellationToken = default)
{
var http = _httpContextAccessor.HttpContext
?? throw new BusinessException("凭证无效");
var appKey = http.Request.Headers[AppKeyHeaderName].FirstOrDefault();
var appSecret = http.Request.Headers[AppSecretHeaderName].FirstOrDefault();
return ResolveAsync(appKey, appSecret, cancellationToken);
}
public async Task<(Customer Customer, CustomerOpenApiCredential Credential)> ResolveAsync(
string? appKey,
string? appSecret,
CancellationToken cancellationToken = default)
{
var key = appKey?.Trim();
var secret = appSecret?.Trim();
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(secret))
{
throw new BusinessException("凭证无效");
}
var credential = await _credentialRepository.GetByAppKeyAsync(key, cancellationToken);
if (credential == null
|| !credential.Enabled
|| !FixedTimeEqualsUtf8(credential.AppSecret, secret))
{
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);
}
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();
}
}