All checks were successful
Build and Deploy / deploy (push) Successful in 56s
推费入参对齐表字段;创建订单凭证走 appKey/appSecret Header,去掉上门揽收与 Body 内 customerId。 Co-authored-by: Cursor <cursoragent@cursor.com>
114 lines
3.9 KiB
C#
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();
|
|
}
|
|
}
|