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(); } }