Rest 把多租户 REST 调用的通用模式抽象成可复用管道。它不是某个功能域的抽象层,而是位于基础设施层(与 RequestLogging 平行),被需要复杂 OAuth 流程、HMAC 签名或 mTLS 的域(如 Jira Cloud)直接引用。有了它,这类域不必各自实现端点解析、凭据管理、鉴权和签名,只描述端点策略和请求即可。
一次调用的内部流程
RestClient(sealed)按固定顺序处理:解析端点 → 解析凭据 → 检查过期 → 打开租约 → 构建消息(鉴权 + 签名)→ 流式发送。响应以 ResponseHeadersRead 读取,正文由调用方按需物化。
协议接口
| 接口 | 职责 |
|---|---|
IRestClient | 执行 SendAsync(context, request),返回 RestResponse |
IRestClock | 可测试的时间来源,提供 UtcNow |
IRestEndpointResolver | 按上下文解析 RestEndpoint(固定端点或多租户) |
IRestHttpClientLeaseFactory | 打开 IRestHttpClientLease,管理 HttpClient 与 mTLS |
IRestOAuthTokenProvider | OAuth2 access token 获取(client-credentials / JWT Bearer) |
IRestPaginator | 分页器,内置 RestLinkPaginator 跟随 RFC Link rel=next |
请求与响应模型
| 类型 | 说明 |
|---|---|
RestRequest | 请求描述:方法、相对路径、查询串、正文、头部、幂等键 |
RestRequestContext | 租户上下文,含 TenantId、CorrelationId,用于凭据解析 |
RestResponse | 流式响应,拥有底层 HttpResponseMessage 和 lease,调用方必须释放 |
RestEndpoint | 固定 origin 的端点策略:鉴权模式、OAuth、mTLS、签名、超时 |
RestConstants | ProviderId = "standard"、ProviderKey = "Rest:standard"、HttpClientName |
RestProviderDescriptor | 通用 REST/Webhook Provider 的能力描述 |
鉴权与签名选项
| 类型 | 说明 |
|---|---|
RestHmacSigningOptions | HMAC 请求签名:时间戳头、签名头、签名编码、密钥字段名 |
RestOAuthJwtBearerOptions | JWT Bearer 断言:issuer、private key、key id、subject |
RestResponse 的几个关键行为:以 ResponseHeadersRead 流式读取,正文不隐式截断;EnsureSuccessStatusCode 映射状态码到 IntegrationFailureKind;实现 IAsyncDisposable,持有 lease 必须释放。
RestClient 的鉴权模式
RestEndpoint.AuthenticationMode 决定 ApplyAuthenticationAsync 如何注入凭据:
| 模式 | 行为 |
|---|---|
None | 不注入鉴权 |
ApiKeyHeader | 写入指定 header(默认由端点配置 header 名) |
Basic | Base64 编码 username:password,写入 Authorization: Basic |
BearerToken | 直接写入 Authorization: Bearer <token> |
OAuth2ClientCredentials | 走 client-credentials grant,由 IRestOAuthTokenProvider 获取 token |
OAuth2JwtBearer | 走 JWT Bearer assertion 换 token |
OAuth2 两种模式都最终拿 Bearer token 写入 Authorization。内置 OAuth 支持仅限 client-credentials grant。
// ① 端点声明用 API Key 鉴权 + HMAC 签名var endpoint = new RestEndpoint( baseAddress: new Uri("https://api.example.com/"), authenticationMode: RestAuthenticationMode.ApiKeyHeader, apiKeyHeaderName: "X-Api-Key", requestSigning: new RestHmacSigningOptions { SignatureHeaderName = "X-Signature", TimestampHeaderName = "X-Timestamp", SecretFieldName = "RequestSigningSecret", SignatureEncoding = WebhookSignatureEncoding.Hex, });HMAC 签名
端点配置了 RequestSigning 时,RestClient.ApplyHmacSignatureAsync 会计算请求签名并写入头部。规范串由时间戳、HTTP 方法、PathAndQuery 和正文 SHA-256 摘要拼接:
// ② 规范串:timestamp\nMETHOD\npathAndQuery\nbodyHashvar canonicalText = string.Join("\n", timestamp, request.Method.Method.ToUpperInvariant(), message.RequestUri!.PathAndQuery, bodyHash);签名用凭据的 UseSecret 在 char[] 有效期内计算 HMAC-SHA256,避免中间 string。计算完成后签名可选 Hex(转小写)或 Base64 编码,写入时间戳头、签名头(可带前缀)和正文哈希头。
错误处理
SendAsync 用一套 try/catch 分层处理异常,全部归一为 RestException:
| 捕获条件 | 处理 |
|---|---|
OperationCanceledException 且 cancellationToken.IsCancellationRequested | 原样重抛(用户主动取消) |
OperationCanceledException(超时) | 包装为 RestException,failureKind = Timeout,isTransient = true |
HttpRequestException / IOException / SocketException / AuthenticationException | 包装为 RestException,failureKind = Network,isTransient = true |
RestException | 原样重抛 |
| 其他 | 不捕获,向上传播 |
HTTP 状态码到 IntegrationFailureKind 的映射发生在 RestResponse.CreateStatusException:
| 状态码 | FailureKind |
|---|---|
| 401 | Authentication |
| 403 | Authorization |
| 404 | NotFound |
| 408、504 | Timeout |
| 409 | Conflict |
| 429 | RateLimited |
| 其他 4xx | Validation |
| 其他 | Provider |
isTransient 对 408、425、429 和所有 5xx 标记为 true。RetryAfter 从响应头的 Retry-After 解析。
RestException
RestException 继承 IntegrationException,额外增加 Operation 字段标识失败操作(如 "send"、"authenticate"、"sign"),Domain 固定为 "Rest":
// ③ RestException 比 IntegrationException 多了 Operationpublic sealed class RestException : IntegrationException{ public string? Operation { get; }}加密卫生
RestClient 对所有承载密钥的 byte[] 在 finally 块中清零,避免密钥残留在托管堆上:
var bytes = Encoding.UTF8.GetBytes(username + ":" + password);try{ message.Headers.Authorization = new AuthenticationHeaderValue( "Basic", Convert.ToBase64String(bytes));}finally{ Array.Clear(bytes, 0, bytes.Length); // ④ Basic 凭据立即清零}HMAC 签名的规范串、签名、密钥 buffer、读取 buffer 都遵循同样的 finally { Array.Clear(...) } 模式。这与 IntegrationCredential 的可清零 char[] 缓冲区一致,贯穿整个密钥处理路径。
DI 注册
Rest 的 DI 扩展位于 Microsoft.Extensions.DependencyInjection,提供三种入口:
// ① 固定端点:凭据仍按每次调用的租户上下文解析services.AddBitzsoftRest(endpoint);
// ② 多租户端点解析器:宿主提供 IRestEndpointResolver 实现services.AddBitzsoftRest<MultiTenantEndpointResolver>();
// ③ 仅注册可复用的 HTTP/mTLS/OAuth/时钟基础设施,不注册端点解析器// 供 typed protocol/Provider 自行组合services.AddBitzsoftRestTransport();AddBitzsoftRest / AddBitzsoftRest<T> 内部调用 AddBitzsoftRestTransport,再注册 IRestClient(RestClient)、IRestPaginator(RestLinkPaginator)和 IWebhookVerifier(RestWebhookVerifier)三项能力,并把验签器注册到 Webhooks 的 verifier 体系。
AddBitzsoftRestTransport 配置命名 HttpClient Bitzsoft.Integrations.Rest,超时设为 InfiniteTimeSpan(由端点级 RestEndpoint.Timeout 控制,默认 2 分钟),并注册默认 handler、IRestClock、IRestHttpClientLeaseFactory、IRestOAuthTokenProvider。
消费示例
public sealed class JiraIssueService(IRestClient rest, IRestClock clock){ public async Task<RestResponse> GetIssueAsync( RestRequestContext context, string issueKey, CancellationToken ct) { var request = new RestRequest { Method = HttpMethod.Get, EscapedRelativePath = $"rest/api/3/issue/{issueKey}", };
var response = await rest.SendAsync(context, request, cancellationToken: ct); response.EnsureSuccessStatusCode("get_issue"); // ⑤ 状态码映射为 RestException return response; }}RestResponse 是流式的——正文只在调用 ReadBodyAsStringAsync / OpenBodyStreamAsync 时才物化,且不施加应用层截断。调用方控制物化时机和保留策略。
位置与边界
相关
- 核心基础设施
- Webhooks 基础设施(Rest 内置验签器注册到 Webhooks 体系)
- Integration Core(
IntegrationCredential、IntegrationException) - 依赖链