Skip to content

Mud.HttpUtils.Client ​

概述 ​

Mud.HttpUtils.Client 是 Mud.HttpUtils 的客户端实现层,提供 IEnhancedHttpClient 的默认实现、内容序列化器、加密提供程序、令牌管理器基类、应用上下文、安全认证、日志脱敏、缓存、健康检查等核心功能。

目标框架 ​

  • netstandard2.0
  • net6.0
  • net8.0
  • net10.0

包含内容 ​

HTTP 客户端实现 ​

类说明
EnhancedHttpClientIEnhancedHttpClient 默认实现,封装 System.Net.Http.HttpClient,支持请求/响应拦截器、基地址动态切换
DirectEnhancedHttpClient internal直接构造的增强客户端,支持加密操作
HttpClientFactoryEnhancedClient基于 IHttpClientFactory 的增强客户端,支持基地址动态切换
EnhancedHttpClientFactory internalIEnhancedHttpClientFactory 默认实现,按名称创建并缓存客户端实例,.NET 8+ 通过 Keyed Service 解析
HttpClientResolverIHttpClientResolver 默认实现,管理命名客户端注册与解析

DirectEnhancedHttpClient 与 EnhancedHttpClientFactory 为 internal 类型,由 AddMudHttpClient 内部使用,通常无需在业务代码中直接引用。

基地址动态切换 ​

EnhancedHttpClient 和 HttpClientFactoryEnhancedClient 均实现了 WithBaseAddress 方法,支持运行时动态切换基地址:

csharp
var userClient = httpClient.WithBaseAddress("https://user-api.example.com");
var orderClient = httpClient.WithBaseAddress("https://order-api.example.com");

// 获取当前基地址
var baseAddress = httpClient.BaseAddress;

WithBaseAddress 创建新的客户端实例,不影响原客户端。新客户端继承原客户端的超时设置和默认请求头。

增强客户端配置选项(EnhancedHttpClientOptions) ​

EnhancedHttpClientOptions 封装了 EnhancedHttpClient 的所有可配置参数,避免构造函数参数过多的问题。此类仅供编程式配置使用,包含接口和委托类型属性,无法通过 IConfiguration 绑定。

属性类型默认值说明
LoggerILogger?null日志记录器实例
RequestInterceptorsIEnumerable<IHttpRequestInterceptor>?null请求拦截器集合
ResponseInterceptorsIEnumerable<IHttpResponseInterceptor>?null响应拦截器集合
SensitiveDataMaskerISensitiveDataMasker?null敏感数据掩码器
AllowCustomBaseUrlsboolfalse是否允许自定义基础 URL(可能带来 SSRF 风险,谨慎使用)
RequestBodySerializationRequestBodySerializationModeDefault请求体序列化模式(Buffered/Streamed 需 ISynchronousContentSerializer;条件不满足时回退默认路径并记一次 Debug 日志,EventId 166)
ExceptionRedactorIExceptionRedactor?null异常擦除器(在异常传播前清除敏感数据)
MaxExceptionContentLengthint?null(生效值 10240)错误响应体最大读取字符数(读取阶段生效,防 OOM)。null 时使用默认值 10240;设为 0 或负数表示不限制。截断时带 ...[已截断] 后缀。日志侧错误响应体窗口 200 字符、反序列化失败日志窗口 500 字符,与本配置独立
CaptureRequestContentboolfalse是否在发送前捕获请求体字符串(用于异常调试)
UrlResolutionUrlResolutionModeDefaultURL 解析模式
MaxSuccessResponseByteslong0成功响应体最大字节数(0 = 不限制;超限抛 ApiRequestException)
HttpVersion net6+Version?null(不干预)写入 HttpRequestMessage.Version;null 时保持构造默认值 1.1
HttpVersionPolicy net6+HttpVersionPolicy?null(不干预)写入 HttpRequestMessage.VersionPolicy;null 时保持构造默认值 RequestVersionOrLower
HttpRequestMessageOptionsDictionary<string, object?>?null写入 HttpRequestMessage.Options 的键值对预设
JsonTypeInfoResolver net8+IJsonTypeInfoResolver?nullNative AOT 下用于 JSON 源生成的类型解析器

配置优先级契约(CFG-01) ​

DI 路径(AddMudHttpClient → CreateEnhancedClient)以 IOptions<EnhancedHttpClientOptions> 为基线克隆,覆盖顺序为:

DI 服务依赖(ILogger / IHttpRequestInterceptor / IHttpResponseInterceptor / ISensitiveDataMasker)
  > MudHttpClients:Clients:<name>.AllowCustomBaseUrls(命名客户端节)
  > services.Configure<EnhancedHttpClientOptions>(...)(编程式配置)
  > 类默认值

注意:IOptions<EnhancedHttpClientOptions>.Value 为单例缓存对象,客户端创建时克隆后覆盖,不会就地修改该单例(多客户端不会串味)。

三条客户端创建路径的能力矩阵(CFG-20) ​

能力① DI AddMudHttpClient② DI + 源生成③ 无 DI RestService.ForGenerated<T>(HttpClient, GeneratedClientOptions)
配置载体MudHttpClientApplicationOptions + EnhancedHttpClientOptions同 ①GeneratedClientOptions
RequestBodySerialization / UrlResolution / JsonTypeInfoResolver✅ 生效(CFG-01 修复后)✅⚠️ 无该能力
SensitiveDataMasker✅✅✅(CFG-06 接线)
RequestInterceptor / ResponseInterceptor✅✅❌ 该路径不适用(无 DI 生成路径不经过 EnhancedHttpClient)
Logger✅(DI)✅⚠️ 固定 NullLogger(无 DI 路径)
RestService.ForGenerated<T>(IServiceProvider)——从容器解析全部依赖,不接受 GeneratedClientOptions

HTTP 版本配置注意(CFG-30):本库两条路径均自建 HttpRequestMessage 后调用 HttpClient.SendAsync, 而 HttpClient.DefaultRequestVersion / DefaultVersionPolicy 官方文档明确不适用于 SendAsync (仅作用于 GetAsync/PostAsync 等由 HttpClient 内部创建请求的便捷重载)。 因此如需 HTTP/2、HTTP/3,请通过 EnhancedHttpClientOptions.HttpVersion(或 GeneratedClientOptions.HttpVersion)显式配置。

配置热更新能力矩阵(CFG-36 / F-3) ​

IConfigurationRoot.Reload() 对各配置项的生效情况并不一致,下表为契约(请勿假设「改了配置就生效」):

配置项载体热更新生效时机说明
MudHttpClients:Clients:<name>.AllowCustomBaseUrlsMudHttpClientApplicationOptions✅下一次创建/解析客户端CreateEnhancedClient 每次读取 IOptionsMonitor.CurrentValue
MudHttpClients:AllowedDomainsMudHttpClientApplicationOptions → UrlValidator✅OnChange 立即重放只替换「配置桶」,不清除运行期 UrlValidator.AddAllowedDomain 新增的域名(CFG-34)
MudHttpClients:Clients:<name>.BaseAddress / TimeoutSeconds / DefaultHeaders注册期快照❌需重启注册期由 section.Bind 生成局部快照并被 ConfigureHttpClient 委托闭包捕获;IOptionsMonitor 会更新,但已注册的 HttpClient 配置不会(CFG-36)
EnhancedHttpClientOptions.*(编程式)IOptions<EnhancedHttpClientOptions>❌需重启客户端创建时克隆单例值
TokenRefreshBackground:*TokenRefreshBackgroundOptions✅下一轮刷新周期TokenRefreshHostedService 改用 IOptionsMonitor<T>,每轮循环读取 CurrentValue。ns2.0 Timer 版(TokenRefreshBackgroundService)仍为快照,不支持热更新
MudHttpOpenTelemetry:*局部实例绑定❌需重启OTel SDK 的 TracerProvider/MeterProvider 构建后不可变;该重载亦不把选项注册进 DI 选项管道
MudHttpTokenRecovery:* / OAuth2:* 等各自 IOptionsMonitor✅依消费方读取时机以各 XXXOptions.SectionName 为准

判据:由 IHttpClientFactory 施加到 HttpClient 上的配置(BaseAddress / Timeout / DefaultRequestHeaders) 仅在注册期读取一次;除非改用 IHttpClientBuilder.ConfigureHttpClient 委托逐次读取(本库当前未采用,见 CFG-36 方案 B)。

AOT JSON 解析器优先级链(CFG-17) ​

IJsonTypeInfoResolver 有多处来源,优先级从高到低:

① EnhancedHttpClientOptions.JsonTypeInfoResolver        (编程式,net8+)
  → ② IOptions<JsonSerializerOptions>.TypeInfoResolver   (DI 注册)
  → ③ MudHttpJsonContext.Default                          (库内置源生成上下文)
  → ④ 反射回退                                            (非 AOT 安全)
  • ①/② 由 HttpContentSerializerFactory.CreateDefault(jsonOptions?.Value, jsonTypeInfoResolver) 合并,来源互不排斥、可叠加。
  • ③ 始终参与合并(AddMudHttpClientJsonContext / AddMudHttpContentSerializer 注册消费方上下文时亦然)。
  • GeneratedClientOptions.JsonTypeInfoResolver(无 DI 路径)不参与该链 —— 该路径的 AOT 元数据请通过 GeneratedClientOptions.ContentSerializer 携带(见 CFG-06)。

可观测性全局开关(MudHttpObservabilityOptions,CFG-D10) ​

MudHttpObservabilityOptions(位于 Mud.HttpUtils.Abstractions)以静态属性提供模块级开关(测试翻转后须在 finally 恢复):

属性默认值说明
RedactUrlInTelemetrytrue是否对 Span tag / 日志 / 诊断事件中的 URL 脱敏(掩码 access_token 等敏感 query 值)
RecordFullUrlOnSuccessfalse成功请求的 Span tag 是否记录完整 URL。默认仅记录 scheme://host/path(不含 query,防泄漏并控制 tag 基数)
MetricTagAllowlist全部内建维度指标 tag 白名单,白名单之外的维度被丢弃(防高基数)
EmitDiagnosticEventstrue是否发出诊断事件(ActivityEvent / DiagnosticSource);关闭为真零分配(调用点门控,不构造 payload/tags 工厂),不影响 Span 与指标
csharp
// 编程式配置 EnhancedHttpClientOptions
services.AddMudHttpClient("myApi", client =>
{
    client.BaseAddress = new Uri("https://api.example.com");
}, setAsDefault: true);

// 通过 EnhancedHttpClientFactoryOptions 为特定客户端配置增强选项
services.Configure<EnhancedHttpClientFactoryOptions>(options =>
{
    options.ClientFactories["myApi"] = sp => new EnhancedHttpClient(
        sp.GetRequiredService<IHttpClientFactory>().CreateClient("myApi"),
        new EnhancedHttpClientOptions
        {
            AllowCustomBaseUrls = false,
            MaxExceptionContentLength = 4096,
            CaptureRequestContent = true,
            RequestBodySerialization = RequestBodySerializationMode.Buffered,
        });
});

注意:AllowCustomBaseUrls 的值在通过 AddMudHttpClientsFromConfiguration 注册时,会被 MudHttpClientOptions.AllowCustomBaseUrls 的值覆盖。JsonTypeInfoResolver 与 DI 注入的 IOptions<JsonSerializerOptions> 中的 TypeInfoResolver 二选一即可,优先级为:JsonTypeInfoResolver → IOptions<JsonSerializerOptions>.TypeInfoResolver → 静态默认。

文件上传进度报告 ​

类说明
ProgressableStreamContent支持进度报告的 HttpContent 实现,用于文件上传场景
csharp
var content = new ProgressableStreamContent(
    fileContent,
    new Progress<long>(bytesRead => Console.WriteLine($"已上传: {bytesRead} 字节")),
    bufferSize: 8192
);

ProgressableStreamContent 在序列化流时通过 IProgress<long> 报告已发送字节数,适用于大文件上传进度监控。

请求/响应拦截器 ​

类说明
IHttpRequestInterceptor请求拦截器接口
IHttpResponseInterceptor响应拦截器接口

拦截器按 Order 属性排序执行,Order 值小的先执行。

响应缓存 ​

类说明
CacheResponseInterceptor响应缓存拦截器,实现 ICacheResponseInterceptor,配合 CacheAttribute 使用
MemoryHttpResponseCache基于 IMemoryCache 的内存响应缓存,实现 IHttpResponseCache
csharp
// 注册缓存拦截器
services.AddSingleton<IHttpResponseCache, MemoryHttpResponseCache>();
services.AddSingleton<IHttpResponseInterceptor, CacheResponseInterceptor>();

CacheResponseInterceptor 的 Order 为 100,确保在其他拦截器之后执行。MemoryHttpResponseCache 使用 IMemoryCache 作为底层存储,支持绝对过期和滑动过期。

注意:不建议将 Response<T> 返回类型与 [Cache] 特性组合使用。缓存会存储整个 Response<T> 对象(包括 StatusCode 和 ResponseHeaders),可能导致后续请求返回过期的状态码和响应头。源代码生成器会对此组合发出 HTTPCLIENT011 编译警告。

加密提供程序 ​

类说明
DefaultAesEncryptionProviderIEncryptionProvider 默认实现,始终使用认证加密(AesGcm 或 CBC + HMAC-SHA256)

密文信封格式 ​

加密产出的密文首字节为信封版本前缀,解密仅按该前缀分派,不依赖任何运行时配置:

版本布局最小长度产出条件可在哪些目标框架解密
0x02[0x02][nonce(12)][tag(16)][密文](AesGcm,AEAD)29net8.0/net10.0 且 AesGcm.IsSupported 且 RequireCrossRuntimePortable=false(默认)仅 net8.0+
0x03[0x03][IV(16)][MAC(32)][密文](CBC + HMAC-SHA256,Encrypt-then-MAC)65其余运行时;或 RequireCrossRuntimePortable=true全部(netstandard2.0/net6.0/net8.0/net10.0)

版本字节空间:0x00 保留为非法哨兵(永不用作版本);0x01 曾用于 v1 裸 CBC,该路径已随「AES 信封版本前缀歧义消除」移除且编号永久冻结;0x04~0x0F 保留给未来对称算法;0x10~0xFF 保留给未来扩展。

无法识别的格式会抛 CryptographicException(消息含实际首字节);完整性校验失败同样抛 CryptographicException。

跨运行时场景:IEncryptableHttpClient.EncryptContent 产出的密文会经 HTTP 传输到对端,而对端目标框架未知。若对端可能低于 net8.0,请在加密侧设置 RequireCrossRuntimePortable = true 强制产出 0x03。

csharp
services.AddMudHttpClient("myApi", encryption =>
{
    encryption.Key = Convert.FromBase64String("your-base64-key");
    // 注意:从 v1.8.0 起 IV 自动随机生成,无需手动设置

    // 可选:密文需能被 netstandard2.0 / net6.0 端解密时开启
    encryption.RequireCrossRuntimePortable = true;
}, client =>
{
    client.BaseAddress = new Uri("https://api.example.com");
});

密钥长度支持 AES-128(16 字节)、AES-192(24 字节)、AES-256(32 字节)。AesEncryptionOptions.Validate() 方法在 IEncryptionProvider 首次解析时验证密钥的有效性。从 v1.8.0 起,IV 在每次加密时自动随机生成;CFG-27 已移除 AesEncryptionOptions.IV 属性(运行时无消费点),无需也无法手动设置。认证加密始终开启,不再提供关闭选项。

也可直接从配置节注册:

csharp
builder.Services.AddMudHttpAesEncryptionFromConfiguration(builder.Configuration);
json
{
  "MudHttpAesEncryption": {
    "Key": "base64-encoded-16/24/32-byte-key"
  }
}

安全认证提供程序 ​

类说明
DefaultApiKeyProviderIApiKeyProvider 默认实现,从 IConfiguration 读取 API Key
DefaultHmacSignatureProviderIHmacSignatureProvider 默认实现,使用 HMAC-SHA256 算法
csharp
// API Key 认证
services.AddSingleton<IApiKeyProvider, DefaultApiKeyProvider>();

// HMAC 签名认证
services.AddSingleton<IHmacSignatureProvider, DefaultHmacSignatureProvider>();

DefaultApiKeyProvider 从 IConfiguration 的 ApiKey 或 ApiKeys:Default 键读取密钥。DefaultHmacSignatureProvider 使用 HMAC-SHA256 算法对请求内容计算签名,签名结果以 Base64 编码。

HTTP 内容序列化器 ​

类说明
SystemTextJsonContentSerializerIHttpContentSerializer 默认实现,基于 System.Text.Json,行为等价于直接调用 JsonSerializer,保证向后兼容
HttpContentSerializerFactory序列化选项合并工厂,集中构建 JsonSerializerOptions,自动合并消费方 resolver(IOptions/编程式)+ 库内置 MudHttpJsonContext.Default +(JIT)反射兜底

所有 JSON 序列化/反序列化(请求体、响应、NDJSON 流式解析、加密内容等)均通过 IHttpContentSerializer 抽象进行,不再直接调用 JsonSerializer。默认实现 SystemTextJsonContentSerializer 可在 DI 中替换为自定义实现以切换序列化引擎(如 Newtonsoft.Json / XML,见 序列化器适配包)。HttpContentSerializerFactory.BuildOptions 会自动把消费方 resolver 与库内置 MudHttpJsonContext.Default 合并:AOT 环境下仅保留源生成上下文、杜绝静默回退反射;JIT 环境下额外 Combine DefaultJsonTypeInfoResolver 兼容未声明类型。

日志脱敏 ​

类说明
DefaultSensitiveDataMasker非 AOT 的反射式实现:自动读取属性上的 [SensitiveData] 并脱敏,支持 Hide、Mask、TypeOnly 三种模式。已标注 [Obsolete](AOT 不安全)
AotSafeSensitiveDataMaskerAOT 安全的编译期字典式实现:忽略 [SensitiveData],必须通过 Register<T>(...) 显式登记 DTO;未登记类型返回 [TypeName]

[SensitiveData] 生效前提(CFG-33,三态必须区分):

  1. 未注册掩码器 ⇒ 特性完全无效(AddMudHttpClient 不会默认注册掩码器;DI 中 ISensitiveDataMasker 为 null);
  2. services.AddSensitiveDataMasker() ⇒ 注册的是 AotSafeSensitiveDataMasker,它按设计忽略 [SensitiveData] ⇒ 特性仍然无效(需改用 Register<T> 登记类型);
  3. 仅 services.AddSensitiveDataMasker<DefaultSensitiveDataMasker>()(或手动 AddSingleton<ISensitiveDataMasker, DefaultSensitiveDataMasker>())才会反射读取 [SensitiveData] —— 该实现非 AOT 安全。

上述任一「无效」情形都不会产生编译期或运行期提示,请务必按需求显式选择实现。

csharp
// 非 AOT:反射读取 [SensitiveData](CFG-33 第 ③ 态)
services.AddSensitiveDataMasker<DefaultSensitiveDataMasker>();

// AOT:编译期字典式,需显式 Register<T>(CFG-33 第 ② 态;[SensitiveData] 不参与)
services.AddSensitiveDataMasker();

// 使用
var masker = serviceProvider.GetRequiredService<ISensitiveDataMasker>();
var masked = masker.Mask("13800138000", SensitiveDataMaskMode.Mask, 3, 4);
// 结果: "138****8000"

var maskedObj = masker.MaskObject(userRequest);
// 自动识别 [SensitiveData] 标记的属性并脱敏

令牌管理 ​

类说明
TokenManagerBase abstract令牌管理器抽象基类(定义于 Abstractions),提供并发安全的令牌刷新,支持绝对过期保护
UserTokenManagerBase abstract用户令牌管理器抽象基类(定义于 Abstractions),提供用户级并发安全刷新和缓存容量控制
StandardOAuth2TokenManagerOAuth2 标准令牌管理器,内置 Authorization Code / Client Credentials / ROPC / Refresh Token 流程
TokenRefreshHostedService.NET 6+ 下的令牌后台刷新服务,实现 IHostedService 和 ITokenRefreshBackgroundService(netstandard2.0 下为 TokenRefreshBackgroundService,基于 Timer)
TokenRefreshBackgroundServicenetstandard2.0 下的令牌后台刷新服务,基于 Timer 定时刷新
TokenRecoveryExecutor401 令牌刷新重试执行器,被 TokenRecoveryDelegatingHandler 与恢复客户端共享
TokenRecoveryDelegatingHandler令牌恢复委托处理器,401 响应时自动刷新令牌并重试,支持多种注入模式
TokenRecoveryEnhancedClient带令牌恢复的增强客户端,继承 HttpClientFactoryEnhancedClient
DefaultTokenProvider internalITokenProvider 默认实现(internal),通过 IMudAppContext 获取令牌管理器并获取令牌
DefaultCurrentUserContext<TUser>ICurrentUserContext 默认实现(泛型,TUser : CurrentUserInfo, new()),使用 AsyncLocal 实现线程安全的用户 ID 传播
MemoryTokenStoreITokenStore 内存默认实现,支持 GetTokenTypesAsync、ClearAsync 批量操作
MemoryUserTokenStoreIUserTokenStore 内存默认实现,按用户 ID 隔离,支持 ClearUserAsync 等
MemoryEncryptedTokenStoreIEncryptedTokenStore 内存默认实现,自动加密/解密令牌数据
MemoryCacheTokenCache<T>ITokenCache<T> 内存缓存实现(基于 IMemoryCache),供 TokenManagerBase 使用
EncryptedTokenCache<T>ITokenCache<T> 加密包装:值经 IEncryptionProvider 加密后以密文驻留内存(SR-M8),配合 UserTokenManagerBase 加密构造重载使用
OAuth2TokenExceptionOAuth2 令牌请求失败的类型化异常(继承 InvalidOperationException,携带 ErrorCode / ErrorDescription / HttpStatusCode)
DelegateTokenManagerRegistryITokenManagerRegistry 委托实现,配合 AddTokenManagerRegistry 为 401 恢复提供按键路由
DefaultFormContentIFormContent 默认实现,基于 Dictionary<string, string>

TokenManagerBase 与 UserTokenManagerBase 的抽象基类定义位于 Mud.HttpUtils.Abstractions 包;OAuth2TokenManagerBase(OAuth2 抽象基类)亦定义于 Abstractions。DefaultTokenProvider 为 internal 类型,由框架在内部使用。DefaultCurrentUserContext<TUser> 为泛型实现,使用时需指定用户类型(如 DefaultCurrentUserContext<MyUser>,MyUser 继承 CurrentUserInfo)。

csharp
// 自定义令牌管理器
public class MyTokenManager : TokenManagerBase
{
    protected override async Task<CredentialToken> RefreshTokenCoreAsync(CancellationToken ct)
    {
        var response = await FetchTokenAsync(ct);
        return new CredentialToken
        {
            AccessToken = response.AccessToken,
            Expire = response.ExpireTime
        };
    }

    public override Task<string> GetTokenAsync(CancellationToken ct = default)
        => GetOrRefreshTokenAsync(ct);
}

// 注册后台刷新服务(推荐方式)
// AddTokenRefreshBackgroundService 内部自动注册为 IHostedService 和 ITokenRefreshBackgroundService,
// 确保两者解析到同一单例实例,消费方可直接注入 ITokenRefreshBackgroundService。
services.AddTokenRefreshBackgroundService(options =>
{
    options.Enabled = true;
    options.RefreshIntervalSeconds = 3500;
    options.RetryDelaySeconds = 60;
    options.StopOnError = false;
});

TokenManagerBase 使用 SemaphoreSlim(1, 1) 确保同一时刻只有一个线程执行令牌刷新。UserTokenManagerBase 使用 IMemoryCache 管理用户令牌缓存,支持 SizeLimit 限制和自动过期清理。TokenRefreshHostedService 支持配置 RefreshIntervalSeconds(刷新间隔)、RetryDelaySeconds(重试延迟)和 StopOnError(出错时是否停止)。非用户令牌的过期提前量由 TokenManagerBase.ExpireThresholdSeconds 控制(默认 300 秒,引用 TokenManagerBase.DefaultExpireThresholdSeconds 常量);用户令牌的过期提前量由 UserTokenCacheOptions.ExpireThresholdSeconds 控制(默认同样为 300 秒,引用同一常量),可通过 AddMudHttpUserTokenCacheFromConfiguration 绑定。

DefaultTokenProvider 是 ITokenProvider 的默认实现,通过 IMudAppContext 获取令牌管理器并获取令牌。它不持有 IMudAppContext 引用,而是通过方法参数逐调用接收,以确保生成代码中 UseApp()/UseDefaultApp() 上下文切换的正确性。当 TokenRequest.UserId 非空时,自动使用 IUserTokenManager 获取用户级令牌。

DefaultCurrentUserContext 使用 AsyncLocal 确保用户 ID 在异步上下文中正确传播。适用于非 Web 场景或需要手动设置用户 ID 的场景。在 ASP.NET Core 应用中,建议替换为基于 HttpContext 的实现。每个实例拥有独立的 AsyncLocal 存储,支持多实例并行使用。通过 SetUser(TUser? user) 实例方法设置当前用户对象(TUser 须继承 CurrentUserInfo 且具有无参构造函数),也可通过 SetUserId(string? userId) 方法直接设置用户 ID,UserId 属性从用户对象中自动提取。

内存令牌存储 ​

csharp
// 基础内存存储
services.AddSingleton<ITokenStore, MemoryTokenStore>();

// 用户级内存存储
services.AddSingleton<IUserTokenStore, MemoryUserTokenStore>();

// 加密内存存储(需先注册 IEncryptionProvider)
services.AddSingleton<IEncryptionProvider, DefaultAesEncryptionProvider>(/* 配置密钥 */);
services.AddSingleton<IEncryptedTokenStore, MemoryEncryptedTokenStore>();

MemoryTokenStore 基于 ConcurrentDictionary 实现线程安全的令牌管理,支持过期自动清理。MemoryUserTokenStore 为每个用户维护独立的存储空间(外层 userId 比较器为 Ordinal——"User1" 与 "user1" 是两个用户,大小写归一化责任在调用方入口)。MemoryEncryptedTokenStore 在存储前自动加密令牌数据,读取时自动解密,适用于对安全性要求较高的场景。

令牌管理安全加固(SR 轮行为要点) ​

能力说明
租户绑定守卫(SR-H5)TokenManagerBase bind-once:单实例被不同 AppKey 共享时快速失败(合法共享场景覆写 EnforceTenantBinding = false)
用户令牌按 scope 隔离(SR-M1)GetOrRefreshTokenAsync(userId, scopes) 按 userId × scope 复合键隔离缓存与锁;RemoveTokenAsync(userId) / InvalidateUserTokenAsync(userId) 清除该用户全部作用域(登出语义);刷新失败负缓存指数退避(30s→60s→120s→240s→300s 封顶)
内存态加密缓存(SR-M8)UserTokenManagerBase 构造重载传入 IEncryptionProvider 即以 EncryptedTokenCache<T> 包装默认缓存,密文损坏按 miss 处理触发重新获取
401 恢复按键路由(SR-M6)AddTokenManagerRegistry 注册解析委托后,恢复执行器按 TokenRecoveryContext.TokenManagerKey 路由失效/刷新/重试全链路(含用户级;解析到非用户管理器一律回退注入实例);解析失败回退 + Warning
请求体三态处理(TMR-01/02)401 恢复的请求体缓冲含 chunked 硬上限(MaxCachedRequestBodyBytes,默认 1MB);三态模型:无体正常恢复、可缓冲首次与重试同源、不可缓冲原样发送但放弃重试(返回真实 401)。MaxCachedRequestBodyBytes = 0 = 流式优先模式(不缓冲、不重试,但正常发送)
userId 一致性校验(SR-M7/L2)恢复执行器与 DefaultTokenProvider 校验 TokenRecoveryContext.UserId 与受信 ICurrentUserContext.UserId 一致性,不一致即拒绝

默认表单内容 ​

csharp
var formData = new Dictionary<string, string>
{
    ["username"] = "admin",
    ["password"] = "secret"
};
var formContent = new DefaultFormContent(formData);
var httpContent = formContent.ToHttpContent(); // FormUrlEncodedContent

DefaultFormContent 是 IFormContent 的默认实现,将字典数据转换为 FormUrlEncodedContent。适用于简单的表单提交场景。

OAuth2 配置 ​

OAuth2Options 用于配置 OAuth2 客户端凭证流程的参数,配置节名称为 MudHttpOAuth2。

属性类型默认值说明
ClientIdstring""客户端 ID
ClientSecretstring""客户端密钥(明文,建议优先使用 ClientSecretProviderName)
ClientSecretProviderNamestring?null密钥安全提供程序名称,设置后从 ISecretProvider 获取密钥
TokenEndpointstring""令牌端点 URL
RevocationEndpointstring""令牌撤销端点 URL
IntrospectionEndpointstring""令牌内省端点 URL
RequireHttpsbooltrue是否强制 HTTPS 端点
ExpirySafetyMarginSecondsint60令牌过期安全边际(秒),提前刷新以避免使用过期令牌
ClientSecretCacheTtlSecondsint300密钥缓存 TTL(秒),密钥轮换后 TTL 过期即重新解析(0 = 不缓存;未启用安全提供程序时不进入缓存路径)
AllowDefaultScopeRefreshTokenFallbackboolfalse是否允许当前作用域缺 refresh_token 时回退默认作用域凭据(默认关闭,防跨作用域凭据串用;仅 IdP 支持"统一刷新令牌"时显式开启)

安全提示:当同时设置 ClientSecret 和 ClientSecretProviderName 时,ClientSecretProviderName 优先生效。建议仅设置其中之一以避免混淆。AddMudHttpOAuth2FromConfiguration 会在启动时自动检测此冲突并记录警告日志。

csharp
// 通过代码配置
services.Configure<OAuth2Options>(options =>
{
    options.ClientId = "my-client";
    options.ClientSecretProviderName = "vault-provider";
    options.TokenEndpoint = "https://auth.example.com/token";
    options.ExpirySafetyMarginSeconds = 90;
});

// 或通过 IConfiguration 绑定
services.AddMudHttpOAuth2FromConfiguration(configuration);

对应 appsettings.json:

json
{
  "MudHttpOAuth2": {
    "ClientId": "my-client",
    "ClientSecretProviderName": "vault-provider",
    "TokenEndpoint": "https://auth.example.com/token",
    "RevocationEndpoint": "https://auth.example.com/revoke",
    "IntrospectionEndpoint": "https://auth.example.com/introspect",
    "RequireHttps": true,
    "ExpirySafetyMarginSeconds": 90
  }
}

密钥缓存 TTL 热更新(L-10):ClientSecretCacheTtlSeconds 支持 IOptionsMonitor 热更新 —— TTL 在每次解析密钥时读取当前值,修改配置无需重建令牌管理器(重建会连带丢失令牌缓存)。设为 0 表示不缓存。

用户令牌缓存配置 ​

UserTokenCacheOptions 用于配置用户令牌缓存的容量、过期和清理策略,配置节名称为 MudHttpUserTokenCache。

属性类型默认值说明
SizeLimitint10000缓存容量限制(SR-M1 起计数单位为用户 × 作用域条目,容量规划按活跃授权组合估算)
ExpireThresholdSecondsint300令牌过期提前量(秒),即将过期时触发刷新
CleanupIntervalSecondsint300缓存清理间隔(秒)
SlidingExpirationSecondsint3600滑动过期时间(秒),未访问则自动移除
CompactionPercentagedouble0.2缓存压缩百分比(达容量限制时按此比例淘汰)
csharp
// 通过 IConfiguration 绑定
services.AddMudHttpUserTokenCacheFromConfiguration(configuration);

UserTokenManagerBase 支持通过 IOptions<UserTokenCacheOptions> 从 DI 注入缓存配置。子类构造函数可接收 IOptions<UserTokenCacheOptions> 参数,确保通过 AddMudHttpUserTokenCacheFromConfiguration 绑定的配置生效。

响应缓存配置 ​

ResponseCacheOptions 用于控制内存响应缓存的容量与清理策略。该选项作为 MudHttpClientApplicationOptions.ResponseCache 子节绑定,也可通过 AddHttpResponseCache 扩展方法的参数进行设置。

属性类型默认值说明
MaxCacheSizeint1000最大缓存条目数,超出后采用 LRU 淘汰
CleanupIntervalSecondsint60过期缓存清理间隔(秒)
csharp
// 通过 AddHttpResponseCache 扩展方法指定参数
services.AddHttpResponseCache(maxCacheSize: 2000, cleanupIntervalSeconds: 120);

// 或从 MudHttpClientApplicationOptions 配置节绑定
// appsettings.json:
// "MudHttpClients": {
//   "ResponseCache": {
//     "MaxCacheSize": 2000,
//     "CleanupIntervalSeconds": 120
//   }
// }
services.AddMudHttpClientsFromConfiguration(configuration);

当同时调用 AddHttpResponseCache 并在 MudHttpClients:ResponseCache 配置节中设置值时,两者均使用 TryAddSingleton 语义注册——先注册者生效。通常建议二选一:

  • 如需从配置文件控制缓存参数,使用 AddMudHttpClientsFromConfiguration(内部自动读取 ResponseCache 子节)。
  • 如需代码硬编码缓存参数,使用 AddHttpResponseCache(maxCacheSize, cleanupIntervalSeconds)。
  • 如需完全自定义缓存实现,直接注册 IHttpResponseCache。

CFG-16:两者同时配置且配置节设置了非默认值时,启动期记录警告日志(EventId 117,"响应缓存双入口同时配置"),避免配置被静默忽略。

令牌恢复配置 ​

TokenRecoveryOptions 用于控制 401 响应时的自动令牌刷新与重试行为,配置节名称为 MudHttpTokenRecovery。

属性类型默认值说明
Enabledbooltrue是否启用令牌恢复机制
RecoveryMaxRetriesint1令牌恢复的最大重试次数(必须 >= 0,启动时由 TokenRecoveryOptionsValidator 校验)
TokenSchemestring"Bearer"令牌的认证方案(不能为空,启动时校验)
RefreshTimeoutSecondsdouble30令牌刷新的超时兜底(秒),取消隔离后刷新任务仅受本超时约束
MaxCachedRequestBodyByteslong1048576401 恢复可缓冲的请求体上限(字节),默认 1MB;三态模型:超限/0 = 不缓冲但正常发送(返回真实 401,不重试)
RefreshDedupWindowSecondsdouble2令牌刷新去重窗口(秒),窗口内并发 401 共享同一次刷新结果,窗口过期后触发新一轮刷新
csharp
// 通过代码配置
services.Configure<TokenRecoveryOptions>(options =>
{
    options.Enabled = true;
    options.RecoveryMaxRetries = 2;
    options.TokenScheme = "Bearer";
});

// 或通过 IConfiguration 绑定
services.AddMudHttpTokenRecoveryFromConfiguration(configuration);
json
{
  "MudHttpTokenRecovery": {
    "Enabled": true,
    "RecoveryMaxRetries": 2,
    "TokenScheme": "Bearer"
  }
}

令牌后台刷新配置 ​

TokenRefreshBackgroundOptions 用于配置令牌主动刷新后台服务,配置节名称为 TokenRefreshBackground。

命名差异:此配置节名称为 TokenRefreshBackground,未遵循其他配置节的 MudHttp 前缀命名约定,为向后兼容历史版本而保留。下个大版本将统一为 MudHttpTokenRefreshBackground。

属性类型默认值说明
Enabledboolfalse是否启用后台刷新,需显式设置为 true
RefreshIntervalSecondsint300刷新间隔(秒),必须大于 0
RetryDelaySecondsint60刷新失败后重试延迟(秒),必须大于 0
StopOnErrorboolfalse刷新失败时是否停止服务
MaxConsecutiveFailuresint0连续失败周期数达到该阈值时停止服务(0 = 不因连续失败停止,与 StopOnError 正交)
csharp
// 通过代码配置
services.AddTokenRefreshBackgroundService(options =>
{
    options.Enabled = true;
    options.RefreshIntervalSeconds = 3500;
    options.RetryDelaySeconds = 60;
    options.StopOnError = false;
});

RecoveryMaxRetries 设置为负数时将抛出 ArgumentOutOfRangeException。TokenScheme 设置为 null 或空字符串时将抛出 ArgumentException。此外,AddMudHttpTokenRecoveryFromConfiguration 会注册 TokenRecoveryOptionsValidator,在启动时自动校验上述约束。

RefreshIntervalSeconds 和 RetryDelaySeconds 设置为 0 或负数时将抛出 ArgumentOutOfRangeException。此外,AddTokenRefreshBackgroundService 的两个重载(Action<TokenRefreshBackgroundOptions> 与 IConfiguration)均会注册 TokenRefreshBackgroundOptionsValidator,当 RetryDelaySeconds 大于等于 RefreshIntervalSeconds 时返回校验失败(会抛出 OptionsValidationException 阻止启动 —— 重试延迟跨越下一个刷新周期可能导致刷新逻辑混乱)。

停止可观测与恢复(L-9) ​

后台刷新在 StopOnError = true 或连续失败达到 MaxConsecutiveFailures 时会优雅停止调度(仅记 Critical,宿主继续运行)。ITokenRefreshBackgroundService 提供两个成员用于观测与恢复(netstandard2.0 与 net6+ 两条实现语义一致):

成员说明
IsStopped是否已因刷新失败而主动停止调度。宿主正常停止(StopAsync)不会使其为 true —— 可据此区分"服务在跑但没活干"与"已停止调度",适合接入健康检查。
RestartAsync(ct)复位连续失败计数后重新进入调度循环。幂等:服务仍在运行时为无操作;宿主正在停止时不生效。
csharp
var refreshService = serviceProvider.GetRequiredService<ITokenRefreshBackgroundService>();

// 健康检查中暴露
if (refreshService.IsStopped)
{
    // IdP 恢复后手动恢复,无需重启进程
    await refreshService.RestartAsync();
}

修复前不存在任何恢复通道:一次网络抖动导致连续失败达阈值后,该管理器的后台刷新会永久停止,只能重启进程。

应用上下文 ​

应用上下文的接口(IMudAppContext、IAppManager<T>、IAppContextSwitcher)与默认管理器实现(DefaultAppManager<T>)定义于 Mud.HttpUtils.Abstractions 包。本包提供基于 AsyncLocal 的上下文持有器实现。

类说明
AsyncLocalAppContextSwitcherIAppContextHolder 默认实现,基于 AsyncLocal 维护当前应用上下文(Current / BeginScope)
csharp
// 多应用管理(IAppManager<T> 默认实现位于 Mud.HttpUtils.Abstractions)
services.AddSingleton<IAppManager<FeishuContext>, DefaultAppManager<FeishuContext>>();

// 监听配置变更
var appManager = serviceProvider.GetRequiredService<IAppManager<FeishuContext>>();
appManager.ConfigurationChanged += (sender, args) =>
{
    Console.WriteLine($"应用 {args.AppKey} 配置已变更");
};

DefaultAppManager<T> 新增 ConfigurationChanged 事件,支持应用配置热更新通知。IMudAppContext 新增 GetService<T>() 方法,支持从应用上下文中解析 DI 服务。AsyncLocalAppContextSwitcher 实现 IAppContextHolder,用于在当前异步上下文中切换/持有时应用上下文。

多应用接线清单 ​

多应用(多租户)场景需要注册以下服务。使用 AddMudHttpClientsFromConfiguration 配置入口时会自动补齐 IAppContextHolder,其余需显式注册:

隔离机制对应服务/Key注册 API缺失时的症状
应用上下文持有器IAppContextHolderAddMudHttpAppContextHolder()per-app 弹性隔离不可用;DefaultHttpRequestExecutor 无法解析当前 AppKey
应用管理器IAppManager<IMudAppContext>services.AddSingleton<IAppManager<IMudAppContext>, DefaultAppManager<IMudAppContext>>()UseApp/BeginScope(appKey) 不可用;生成代码抛 InvalidOperationException
per-app 弹性策略IAppResiliencePolicyResolverAddMudHttpAppResilience(perAppOptionsFactory)per-app 策略退化为全局策略
应用切换授权器IAppAccessAuthorizerservices.AddSingleton<IAppAccessAuthorizer, YourAuthorizer>();单应用/受信场景用 AllowAllAppAccessAuthorizer 显式放行必须注册(MT-02 / BC-18):未注册时 UseApp / BeginScope(appKey) / UseAppScope(appKey) 直接抛 InvalidOperationException(默认拒绝)
URL 验证器IUrlValidatorAddMudHttpUrlValidator()静态调用与既有行为等价;DI 注册后可按应用隔离白名单

可调用 serviceProvider.ValidateMudHttpAppManagement() 手动校验接线完整性(全 TFM 可用,供 netstandard2.0 宿主与单元测试使用)。也可调用 AddMudHttpHealthChecks() 注册 mud_app_management 健康检查,在 /health 端点观测多应用接线状态。

上下文归还约束(重要) ​

UseApp(appKey) / SwitchTo(...) 是无作用域切换:它们只写入 AsyncLocal,不会自动归还上一个上下文。在 ASP.NET Core 这类长生命周期宿主中,一次未归还的切换会让同一个异步流上的后续请求继续看到上一个应用 —— 即读取到其它租户的令牌。

因此:

  • 请求处理路径:优先使用 UseAppScope(appKey)(using 自动归还)或生成客户端的 BeginScope(appKey)。
csharp
// 推荐:作用域式切换,离开 using 自动归还
using (_client.UseAppScope("app-a"))
{
    await _client.CallApiAsync();
}

// 或不使用作用域,改为显式在 finally 中归还
var previous = _appContextHolder.Current;
try
{
    _appContextHolder.SwitchTo(appAContext);
    await _client.CallApiAsync();
}
finally
{
    _appContextHolder.SwitchTo(previous);   // 必须归还
}
  • 后台任务 / Task.Run / Parallel.ForEach:AsyncLocal 会随执行上下文流式传播到子任务。若后台任务需要独立轮询多个应用,务必在任务内部建立自己的作用域,不要依赖调用方残留的上下文:
csharp
// 后台任务:每个应用独立作用域,互不串扰
foreach (var appKey in appKeys)
{
    await Task.Run(async () =>
    {
        using (_client.UseAppScope(appKey))   // 子任务内建立自己的上下文
        {
            await _client.SyncDataAsync();
        }
    });
}

跨执行上下文的 using 释放不会回滚(AsyncLocal 语义使然):作用域必须在建立它的同一个异步流内释放。若把 BeginScope 的返回值传递给另一个 Task.Run 去 Dispose,回滚不会生效,且可能把陈旧上下文写回。

工具类 ​

类型说明
XmlSerializeXML 序列化/反序列化工具
HttpClientUtilsHTTP 客户端扩展方法
UrlValidatorURL 安全验证工具(可配置域名白名单,支持 SSRF 防护)
MessageSanitizer敏感信息脱敏工具(优化字段检测,减少误判)

HTTP 请求执行器 ​

类说明
DefaultHttpRequestExecutorIHttpRequestExecutor 默认实现,统一处理响应反序列化、错误处理和拦截器调用

DefaultHttpRequestExecutor 是生成的 API 实现类与运行时之间的桥梁,负责发送 HTTP 请求、处理响应反序列化、错误状态码异常抛出、拦截器调用等逻辑。

健康检查 ​

类说明
MudCircuitBreakerHealthCheck熔断器健康检查,报告熔断器当前状态
TokenRefreshHealthCheck令牌刷新健康检查,报告令牌刷新服务状态和最近刷新结果
TokenRefreshHealthCheckOptions令牌刷新健康检查配置选项
csharp
// 注册健康检查
services.AddMudHttpHealthChecks();

// 或从 IConfiguration 绑定
services.AddMudHttpHealthChecks(Configuration);

AddMudHttpHealthChecks() 扩展方法注册熔断器和令牌刷新健康检查,可配合 ASP.NET Core Health Checks 中间件使用。

令牌刷新健康检查选项 ​

TokenRefreshHealthCheckSettings(继承自 TokenRefreshHealthCheckOptions,额外增加 FailureStatus 属性)用于配置令牌刷新健康检查的窗口期和阈值,在 appsettings.json 中位于 MudHttpHealthChecks:TokenRefresh 下。

属性类型默认值说明
WindowSecondsint300统计窗口期(秒)
DegradedThresholddouble0.2告警阈值(失败率 0~1),达到则返回 Degraded
CriticalThresholddouble0.5临界阈值(失败率 0~1),达到则返回 Unhealthy
MinSampleSizeint5最小样本数,窗口期内总刷新次数低于此值时返回 Healthy
FailureStatusHealthStatus?null失败时返回的健康状态(null 表示由健康检查内部判定)

熔断器健康检查选项 ​

CircuitBreakerHealthCheckSettings 用于配置熔断器健康检查,在 appsettings.json 中位于 MudHttpHealthChecks:CircuitBreaker 下。

属性类型默认值说明
MaxOpenCountint0允许的 Open 状态最大数量
MaxHalfOpenCountint0允许的 HalfOpen 状态最大数量
FailureStatusHealthStatus?Unhealthy失败时返回的健康状态

对应 appsettings.json:

json
{
  "MudHttpHealthChecks": {
    "TokenRefresh": {
      "WindowSeconds": 300,
      "DegradedThreshold": 0.2,
      "CriticalThreshold": 0.5,
      "MinSampleSize": 5,
      "FailureStatus": "Degraded"
    },
    "CircuitBreaker": {
      "MaxOpenCount": 0,
      "MaxHalfOpenCount": 0,
      "FailureStatus": "Unhealthy"
    }
  }
}

MudHttpHealthChecks 下的 TokenRefresh 和 CircuitBreaker 子节会被 AddMudHttpHealthChecks(IConfiguration) 自动绑定。

可观测性 ​

类说明
TracingDelegatingHandler追踪委托处理器,自动创建 Activity 并记录 HTTP 请求链路信息
MudHttpObservability internal可观测性辅助工具(internal),提供指标记录和追踪标签管理

TracingDelegatingHandler 作为 DelegatingHandler 注入到 HttpClient 管道中,自动创建分布式追踪 Activity 并记录请求方法、URL、状态码、耗时等信息。配合 Mud.HttpUtils.OpenTelemetry 包可一键导出到 OTLP 收集器。

去重协议(标记先行) ​

多条执行路径共用同一套采集逻辑时,通过请求属性 __mud_observed 去重,协议为**"谁通过检查,谁立即标记;外层观察窗口覆盖整个弹性循环,内层一律短路"**:

场景采集方Span 数请求数据指标
AddMudHttpClient + HttpClientFactoryEnhancedClient(默认组合路径)Enhanced 外层窗口11 组
裸 factory.CreateClient(name).SendAsync(...)(不经过 Enhanced)TracingDelegatingHandler11 组
直连 new EnhancedHttpClient(new HttpClient(...))(无 Handler)Enhanced 外层窗口11 组
组合路径 + Polly 重试(克隆拷贝 __mud_*)仅外层窗口一次(重试次数经 __mud_retry_count / Activity tag 体现)11 组
组合路径 + 令牌恢复(恢复克隆剥离 __mud_*)外层窗口 1 次 + 恢复尝试由管道独立采集2(不同请求)各 1 组

补充语义:

  • 取消:请求被取消(OCE 且调用方令牌已触发)记 outcome=cancelled,Span 不设 Error(OTel 语义);HttpClient 超时仍记 error。
  • 4xx 业务流:4xx 触发 ApiException 的路径记 outcome=client_error + Span Ok(与 Handler 路径一致),5xx/网络错误记 outcome=error + Span Error。
  • 零开销降级:EmitDiagnosticEvents=false 时事件路径零分配(调用点门控 + 惰性 payload/tags 工厂);全部 10 个指标仪表维度受 MetricTagAllowlist 约束(含 ObservableGauge)。

URL 安全验证 ​

UrlValidator 提供 SSRF(服务端请求伪造)防护,支持以下安全策略:

  • 域名白名单:仅允许访问白名单内的域名(含子域名匹配)
  • HTTPS 强制:仅允许 HTTPS 协议和标准端口(443)
  • 私有 IP 检测:阻止访问 10.x、172.16.x、192.168.x、127.x 等私有地址
  • 内网域名检测:阻止访问 .local、.internal、.lan 等内网域名

配置方式一:通过配置文件(推荐)

json
{
  "MudHttpClients": {
    "AllowedDomains": [ "api.example.com", "cdn.example.com" ],
    "Clients": {
      "Default": {
        "BaseAddress": "https://api.example.com",
        "AllowCustomBaseUrls": false
      },
      "ExternalApi": {
        "BaseAddress": "https://external.api.com",
        "AllowCustomBaseUrls": true
      }
    }
  }
}

AllowCustomBaseUrls 默认为 false,仅允许访问白名单域名。设为 true 时放宽域名限制但仍阻止私有 IP 和内网域名。

配置方式二:通过代码

csharp
// 配置白名单
UrlValidator.ConfigureAllowedDomains(["api.example.com", "cdn.example.com"]);

// 运行时增删域名
UrlValidator.AddAllowedDomain("new-api.example.com");
UrlValidator.RemoveAllowedDomain("old-api.example.com");

客户端执行逻辑 ​

Mud.HttpUtils.Client 在运行时承担「请求组装 → 安全处理 → 发送 → 响应处理」的完整链路。下图展示一次 HTTP 请求在客户端层各组件间的流转:

mermaid
flowchart TD
    Start["生成代码 / 业务调用<br/>IHttpRequestExecutor"] --> EX["DefaultHttpRequestExecutor<br/>统一入口:反序列化 / 错误处理 / 拦截器调度"]

    EX --> ReqI["请求拦截器链<br/>IHttpRequestInterceptor(按 Order 升序)"]
    ReqI --> Token["令牌注入<br/>DefaultTokenProvider → IMudAppContext<br/>→ TokenManager / IUserTokenManager"]
    Token --> Enc{"已配置加密?<br/>IEncryptionProvider"}
    Enc -->|"是"| EncOp["请求体 / 字段加密<br/>DefaultAesEncryptionProvider(AesGcm / CBC+HMAC)"]
    Enc -->|"否"| Auth
    EncOp --> Auth["认证头注入<br/>API Key / HMAC 签名"]
    Auth --> UrlCheck["URL 安全校验<br/>UrlValidator(SSRF 防护 / 域名白名单)"]
    UrlCheck -->|"非法地址"| UrlErr["拒绝请求并抛出异常"]
    UrlCheck -->|"通过"| Client["IEnhancedHttpClient 发送"]

    Client --> Mode{"客户端类型"}
    Mode -->|"直接"| EHC["EnhancedHttpClient"]
    Mode -->|"工厂"| FH["HttpClientFactoryEnhancedClient"]
    Mode -->|"带恢复"| TR["TokenRecoveryEnhancedClient"]
    EHC --> Trace["TracingDelegatingHandler<br/>创建 Activity / 记录链路"]
    FH --> Trace
    TR --> Trace
    Trace --> Net["HttpClient(System.Net.Http)"]

    Net -->|"返回响应"| RespI["响应拦截器链<br/>CacheResponseInterceptor(Order=100)"]
    RespI --> Dec{"成功?<br/>2xx"}
    Dec -->|"是"| Deser["反序列化 → T / Response&lt;T&gt;"]
    Dec -->|"否(4xx/5xx)"| Err["抛出状态码异常 / 返回 Response&lt;T&gt;"]

    Net -->|"401 Unauthorized"| Recover["TokenRecoveryDelegatingHandler<br/>→ TokenRecoveryExecutor 刷新令牌并重试"]
    Recover --> Client

令牌获取与 401 恢复流程 ​

令牌的并发安全获取与「401 自动恢复」由客户端层内部协作完成,独立于业务接口,无需在生成代码中显式处理:

mermaid
sequenceDiagram
    participant B as 业务/生成代码
    participant EX as DefaultHttpRequestExecutor
    participant TP as DefaultTokenProvider
    participant CTX as IMudAppContext
    participant TM as TokenManagerBase
    participant OS as TokenRefreshHostedService
    participant RH as TokenRecoveryDelegatingHandler
    participant RE as TokenRecoveryExecutor
    participant NET as HttpClient

    B->>EX: 调用(含 UserId?)
    EX->>TP: GetTokenAsync(TokenRequest)
    TP->>CTX: 获取当前 App / 用户上下文
    CTX-->>TP: TokenManager / IUserTokenManager
    TP->>TM: GetOrRefreshTokenAsync()
    TM->>TM: SemaphoreSlim(1,1) 加锁
    alt 缓存命中且未临近过期
        TM-->>TP: 缓存的 CredentialToken
    else 需刷新
        TM->>TM: RefreshTokenCoreAsync()<br/>(StandardOAuth2TokenManager / 自定义)
        TM-->>TP: 新 CredentialToken
    end
    TP-->>EX: AccessToken
    EX->>NET: 携带 Token 发送请求

    NET-->>RH: 返回 401
    RH->>RE: 触发令牌恢复(≤ RecoveryMaxRetries)
    RE->>TM: 强制刷新令牌
    TM-->>RE: 新令牌
    RE->>NET: 重发请求(带新令牌)
    NET-->>EX: 成功响应

    OS->>TM: 定时主动刷新(RefreshIntervalSeconds)
    TM-->>OS: 更新缓存令牌

要点:

  • 令牌获取零反射、零上下文持有:DefaultTokenProvider 不持有 IMudAppContext,而是通过每次调用的 TokenRequest(含 UserId)接收上下文,确保 UseApp()/UseDefaultApp() 切换正确传播。
  • 并发安全刷新:TokenManagerBase 使用 SemaphoreSlim(1,1) 保证同一时刻仅一个线程刷新;UserTokenManagerBase 通过 IMemoryCache 按用户隔离并控制容量(SizeLimit)。
  • 401 自愈:TokenRecoveryDelegatingHandler 与 TokenRecoveryEnhancedClient 共享 TokenRecoveryExecutor,在 RecoveryMaxRetries 次数内自动刷新并重试,与弹性装饰器的重试互不干扰。
  • 恢复链路租户守卫(L-1):TokenRecoveryExecutor 解析出的令牌管理器与取令牌路径同样受 BindTenantGuard 约束(bind-once:管理器实例绑定首个 AppKey 后,其它 AppKey 使用即被拒绝)。被拒绝时不向调用方抛异常,而是记 TenantBindingRejected 告警(EventId 162)并返回服务端真实 401,与其余恢复失败分支语义一致。守卫跳过条件:管理器非 TokenManagerBase 派生类 / 未注入 IAppContextHolder / 当前无应用上下文 / 管理器覆写 EnforceTenantBinding = false。
  • 后台刷新:TokenRefreshHostedService(.NET 6+)/ TokenRefreshBackgroundService(netstandard2.0)按 RefreshIntervalSeconds 主动刷新,避免临界过期。

安装 ​

xml
<PackageReference Include="Mud.HttpUtils.Client" Version="x.x.x" />

DI 服务注册 ​

AddMudHttpClient — 注册客户端 ​

重载说明
AddMudHttpClient(clientName, configureHttpClient)注册 Named HttpClient 和 IEnhancedHttpClient
AddMudHttpClient(clientName, baseAddress)带基础地址的便捷重载
AddMudHttpClient(clientName, configureEncryption, configureHttpClient)带加密配置的重载,同时注册 IEncryptionProvider

AddMudHttpClient 同时注册 IHttpClientResolver 为单例服务,支持多命名客户端场景。

AddMudHttpClientsFromConfiguration — 从配置文件注册 ​

从 IConfiguration 自动绑定多个 HTTP 客户端配置,支持全局域名白名单和自定义 URL 策略:

json
{
  "MudHttpClients": {
    "AllowedDomains": [ "api.example.com", "cdn.example.com" ],
    "DefaultClientName": "Default",
    "ResponseCache": {
      "MaxCacheSize": 2000,
      "CleanupIntervalSeconds": 120
    },
    "Clients": {
      "Default": {
        "BaseAddress": "https://api.example.com",
        "TimeoutSeconds": 30
      },
      "ExternalApi": {
        "BaseAddress": "https://external.api.com",
        "AllowCustomBaseUrls": true
      }
    }
  }
}
csharp
services.AddMudHttpClientsFromConfiguration(Configuration);

TimeoutSeconds 说明:MudHttpClientOptions.TimeoutSeconds 控制 HttpClient 全局超时(包含所有重试的总时间),与 TimeoutOptions.TimeoutSeconds(Polly 单次请求超时)不同。两者可同时配置,详见 弹性策略层。

注册安全认证服务 ​

csharp
// API Key 认证
services.AddSingleton<IApiKeyProvider, DefaultApiKeyProvider>();

// HMAC 签名认证
services.AddSingleton<IHmacSignatureProvider, DefaultHmacSignatureProvider>();

注册缓存服务 ​

csharp
services.AddMemoryCache();
services.AddSingleton<IHttpResponseCache, MemoryHttpResponseCache>();
services.AddSingleton<IHttpResponseInterceptor, CacheResponseInterceptor>();

注册日志脱敏服务 ​

csharp
services.AddSingleton<ISensitiveDataMasker, DefaultSensitiveDataMasker>();
// 或使用便捷扩展方法
services.AddSensitiveDataMasker();                 // 注册 AotSafeSensitiveDataMasker(编译期字典式,需 Register<T>;忽略 [SensitiveData])
services.AddSensitiveDataMasker<MyMasker>();        // 注册自定义实现
services.AddSensitiveDataMasker<DefaultSensitiveDataMasker>(); // 反射读取 [SensitiveData](非 AOT)

便捷注册扩展方法 ​

除手动 AddSingleton<TInterface, TImpl>() 外,本包还提供一组语义化扩展方法,自动注册对应的默认实现(含可传入自定义实现的泛型重载):

扩展方法说明
AddHttpResponseCache(int maxCacheSize = ResponseCacheOptions.DefaultMaxCacheSize, int cleanupIntervalSeconds = ResponseCacheOptions.DefaultCleanupIntervalSeconds)注册内存响应缓存(等价于 IHttpResponseCache + IHttpResponseInterceptor)
AddSensitiveDataMasker() / AddSensitiveDataMasker<TMasker>()注册敏感数据脱敏器
AddApiKeyProvider() / AddApiKeyProvider<TProvider>()注册 API Key 提供器
AddHmacSignatureProvider() / AddHmacSignatureProvider<TProvider>()注册 HMAC 签名提供器
AddTokenProvider() / AddTokenProvider<TProvider>()注册 Token 提供器(ITokenProvider)
AddCurrentUserContext() / AddCurrentUserContext<TContext>()注册当前用户上下文(ICurrentUserContext)
AddMudHttpOAuth2FromConfiguration(IConfiguration, ...)从 MudHttpOAuth2 配置节绑定 OAuth2 选项
AddMudHttpTokenRecoveryFromConfiguration(IConfiguration, ...)从 MudHttpTokenRecovery 配置节绑定令牌恢复选项
AddMudHttpUserTokenCacheFromConfiguration(IConfiguration, ...)从 MudHttpUserTokenCache 配置节绑定用户令牌缓存选项
AddMudHttpClientsFromConfiguration(IConfiguration, ...)从 MudHttpClients 配置节批量注册命名客户端与域名白名单
AddMudHttpAesEncryptionFromConfiguration(IConfiguration, ...)从 MudHttpAesEncryption 配置节注册 AES 加密提供程序
AddMudHttpAppContextHolder()注册 IAppContextHolder(多租户 per-app 隔离必需)
AddMudHttpUrlValidator()注册 URL 验证器(可按应用隔离域名白名单)
AddMudHttpHealthChecks() / AddMudHttpHealthChecks(IConfiguration)注册熔断器与令牌刷新健康检查

依赖项 ​

包说明
Mud.HttpUtils.Abstractions接口定义
Microsoft.Extensions.HttpIHttpClientFactory 支持
Microsoft.Extensions.Logging.Abstractions日志抽象
Microsoft.Extensions.Options选项模式
Microsoft.Extensions.Caching.Memory内存缓存(UserTokenManagerBase、MemoryHttpResponseCache)

Native AOT 支持 ​

Mud.HttpUtils.Client 是 Native AOT 友好的:所有 JSON 序列化/反序列化统一经过 IHttpContentSerializer 抽象,避免在 AOT 下静默回退反射。

  • 通过 AddMudHttpClientJsonContext(...)(.NET 8+)注册消费方 JsonSerializerContext,由 HttpContentSerializerFactory.BuildOptions 自动与库内置 MudHttpJsonContext.Default 合并。
  • 配合 Mud.HttpUtils.JsonContextScaffolder 脚手架自动生成包含闭合泛型(如 FeishuApiResult<T>)的 JsonSerializerContext,或手动将 [HttpJsonSerializable] 标注类型加入 JsonSerializerContext。
  • SystemTextJsonContentSerializer 在 AOT 环境下仅使用源生成元数据,不在运行时反射。
  • SystemTextJsonContentSerializer 已实现 IAotJsonContentSerializer 接口(.NET 8+),生成器产出的调用点经 IHttpContentSerializer 的 options 槽位传入 JsonTypeInfo<T> 走快车道(SerializeToUtf8Bytes → ByteArrayContent)。AOT 下 ToHttpContent<T> 默认路径也走 Utf8Bytes 纵深防御(P1-4)。

详见 JsonContext 脚手架 与 抽象接口层 的 AOT 章节。

设计原则 ​

  • 默认实现可替换:所有核心接口均提供默认实现,但可通过 DI 替换为自定义实现
  • 线程安全:TokenManagerBase、UserTokenManagerBase、HttpClientResolver 均实现并发安全
  • 资源管理:EnhancedHttpClient 内部正确管理 HttpClient 资源(注:本类未实现 IDisposable,由 IHttpClientFactory 或 AddMudHttpClient 负责生命周期管理)
  • 可观测性:所有关键操作均通过 ILogger 记录日志,支持结构化日志
  • 性能优先:使用 SemaphoreSlim 替代 lock、使用 IMemoryCache 替代 ConcurrentDictionary、支持大文件上传进度报告