admin/src/Oms.Application/Shared/CustomerOpenApiAuthHelper.cs
zzh 87f4cebc9b
All checks were successful
Build and Deploy / deploy (push) Successful in 56s
feat: openApi 统一信封与推费字段对齐,下单鉴权改请求头
推费入参对齐表字段;创建订单凭证走 appKey/appSecret Header,去掉上门揽收与 Body 内 customerId。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-30 12:17:50 +08:00

114 lines
3.9 KiB
C#

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;
/// <summary>
/// 客户 OpenAPI 机机鉴权:按请求头 AppKey + AppSecret 解析主客户。
/// </summary>
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;
}
/// <summary>
/// 从当前请求头读取 appKey / appSecret 并鉴权。
/// </summary>
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();
}
}