深入剖析 YARP 的 Transforms:构建灵活的反向代理转换管道

YARP(Yet Another Reverse Proxy)作为微软推出的高性能 .NET 反向代理库,其最强大也最具特色的能力之一就是 Transforms(转换器)。Transforms 让开发者能够以声明式或命令式的方式精细控制请求与响应在代理过程中的每一个细节,从修改路径、改写请求头,到处理 X-Forwarded 系列头部、注入客户端证书信息等等。可以说,理解 Transforms 是真正掌握 YARP 的关键。本文将系统地剖析 YARP Transforms 的设计哲学、工作机制、内置类型以及如何编写自定义 Transform,帮助你在生产环境中构建灵活而健壮的代理层。

一、Transforms 在 YARP 中的角色

YARP 的代理流程可以拆解为四个阶段:客户端请求进入 → 路由匹配(Routes)与目标选择(Clusters) → 请求转换(Request Transforms) → 转发到后端 → 接收后端响应 → 响应转换(Response Transforms 与 Response Trailer Transforms) → 返回客户端。

Transforms 就嵌在转发的两端,承担"翻译"和"改写"的角色。它们解决的问题非常实际:后端服务期望的路径与外部暴露的路径不一致、需要把网关层的认证信息透传给后端、需要根据租户动态改写 Host、需要剥离敏感的内部头部、需要遵循 RFC 7239 添加 Forwarded 头部以便后端正确识别原始客户端等等。如果没有 Transforms,这些工作都需要在每个后端服务中重复实现,YARP 的价值会大打折扣。

值得强调的是,Transforms 并不是中间件(Middleware)的替代品。中间件作用于整个 ASP.NET Core 管道,能够在路由匹配之前或之后做更宽泛的事情;而 Transforms 是与 Route 强绑定的,每条路由可以拥有自己独立的一组 Transforms,这种细粒度的绑定让多租户、多后端的复杂场景变得可控。

二、配置 Transforms 的两种方式

YARP 提供了配置文件驱动和代码驱动两种方式,二者可以共存,且代码驱动的优先级更高。

配置文件方式

最常见的入口是 appsettings.json。Transforms 以一个数组的形式附加到 Route 上,每一个数组元素是一个键值对字典,字典中的键决定了这是哪一种转换。来看一个典型示例:

json 复制代码
{
  "ReverseProxy": {
    "Routes": {
      "api-route": {
        "ClusterId": "api-cluster",
        "Match": {
          "Path": "/api/{**catch-all}"
        },
        "Transforms": [
          { "PathRemovePrefix": "/api" },
          { "PathPrefix": "/v2" },
          { "RequestHeader": "X-Tenant", "Set": "contoso" },
          { "RequestHeader": "X-Forwarded-For", "Append": "{RemoteIpAddress}" },
          { "ResponseHeader": "X-Powered-By", "Set": "YARP", "When": "Always" }
        ]
      }
    },
    "Clusters": {
      "api-cluster": {
        "Destinations": {
          "d1": { "Address": "https://backend.internal/" }
        }
      }
    }
  }
}

这种方式的优点是热更新 。YARP 监听配置变化,修改 appsettings.json 后无需重启即可生效,非常适合需要运营人员快速调整规则的场景。

代码方式

代码方式通过 AddTransforms 扩展方法或在路由构建时使用 WithTransform 链式调用实现:

csharp 复制代码
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
    .AddTransforms(builderContext =>
    {
        // 对所有路由生效
        builderContext.AddRequestHeader("X-Gateway", "yarp", append: false);

        // 仅对特定路由生效
        if (builderContext.Route.RouteId == "api-route")
        {
            builderContext.AddPathPrefix("/v2");
            builderContext.AddRequestTransform(async transformContext =>
            {
                var userId = transformContext.HttpContext.User
                    .FindFirst("sub")?.Value;
                if (!string.IsNullOrEmpty(userId))
                {
                    transformContext.ProxyRequest.Headers.Add("X-User-Id", userId);
                }
            });
        }
    });

代码方式的最大优势是类型安全完整的运行时上下文 :你可以访问 HttpContext、依赖注入的服务、用户身份信息等等,这是配置文件难以企及的灵活度。

三、内置请求 Transforms 详解

YARP 内置了一系列覆盖绝大多数常见场景的 Transforms。下面按功能类别逐一介绍。

路径相关

PathPrefix 在请求路径前添加固定前缀,PathRemovePrefix 反之,移除指定前缀。两者常常配合使用,例如外部暴露 /api/users,后端期望 /v2/users,那么就先 PathRemovePrefix: /api,再 PathPrefix: /v2PathSet 直接将路径替换为一个固定值,使用场景较窄但对于聚合多个后端到同一路径很有用。

最强大的是 PathPattern,它支持基于路由模板变量进行路径重写。如果你的路由匹配 /customers/{id}/orders,那么就可以通过 { "PathPattern": "/api/v1/customers/{id}/orders/list" } 把变量插入到新路径中,这种基于模板的改写能力让 RESTful 路径迁移变得非常优雅。

查询参数相关

QueryValueParameter 用于添加或覆盖一个固定值的查询参数,QueryRouteParameter 则是从路由参数中取值放入查询字符串,QueryRemoveParameter 用于剥离敏感或冗余的参数。一个常见用法是把路径中的租户标识转换为查询参数传给后端,避免后端理解多层路径结构。

请求头相关

RequestHeader 是日常使用最频繁的转换之一,支持 Set(覆盖)、Append(追加)两种语义。需要特别注意的是,Append 对于多值头部(如 CookieX-Forwarded-For)是符合 HTTP 语义的,但对单值头部使用时要小心,因为多个值会以逗号分隔。

RequestHeaderRouteValue 把路由参数注入请求头,对于把 URL 中的标识传递给后端很方便。RequestHeadersAllowed 提供了一种白名单 机制,只允许列表中的请求头透传给后端,其他全部剥离,这在零信任网关场景中是重要的安全措施。RequestHeaderRemove 则是黑名单方式的删除。

RequestHeaderOriginalHost 控制是否把客户端发来的 Host 头透传给后端。默认情况下 YARP 会用集群目标的 Host,但很多多租户后端依赖 Host 头部做虚拟主机分发,这时就需要把它显式设为 true

X-Forwarded 与 Forwarded

代理场景下后端如何知道真实客户端 IP、原始请求的协议(HTTPS 或 HTTP)以及原始 Host?这正是 X-Forwarded-* 系列头部解决的问题。YARP 默认就会添加一组 X-Forwarded-ForX-Forwarded-ProtoX-Forwarded-HostX-Forwarded-Prefix。你可以通过 X-Forwarded 这个 Transform 精细控制其行为:

json 复制代码
{
  "X-Forwarded": "Set",
  "HeaderPrefix": "X-Forwarded-",
  "For": "Append",
  "Proto": "Set",
  "Host": "Off",
  "Prefix": "Set"
}

如果你的环境希望使用 RFC 7239 标准的 Forwarded 头(语法形如 Forwarded: for=192.0.2.43;proto=https),可以使用 Forwarded Transform。两者通常二选一,否则后端可能会得到冗余甚至冲突的信息。

客户端证书与 HTTP 方法

如果代理层做了 mTLS 终结而后端需要客户端证书信息,可以用 ClientCert Transform 把证书以 PEM 编码注入到自定义请求头中。HttpMethodChange 则用于改写 HTTP 方法,例如把外部的 GET 转成内部的 POST,主要用于兼容遗留系统。

四、响应与 Trailer Transforms

响应方向上,最常用的是 ResponseHeaderResponseTrailer,它们在语义上与 RequestHeader 对称。一个有趣且容易被忽略的字段是 When,它控制 Transform 在何种情况下生效:Success(仅响应状态码 2xx/3xx 时)、Failure(仅 4xx/5xx 时)、Always。例如你可能希望只在成功响应中添加缓存控制头,但失败时插入诊断头:

json 复制代码
{ "ResponseHeader": "Cache-Control", "Set": "public, max-age=3600", "When": "Success" },
{ "ResponseHeader": "X-Error-Trace-Id", "Set": "{TraceId}", "When": "Failure" }

类似请求方向,YARP 也提供 ResponseHeadersAllowedResponseHeadersCopyResponseTrailersCopy 等用于控制头部从后端到客户端的流动。

五、自定义 Transform:突破内置能力的边界

当内置 Transforms 不足以表达你的需求时(这在生产中相当常见,例如需要查询数据库、调用外部服务、做复杂的鉴权决策等),就需要编写自定义 Transform。YARP 提供了从最轻量到最系统化的多种扩展点。

内联委托

最简单的方式是在 AddTransforms 中直接传入异步委托:

csharp 复制代码
.AddTransforms(context =>
{
    context.AddRequestTransform(async transformContext =>
    {
        var token = await tokenService.GetServiceTokenAsync();
        transformContext.ProxyRequest.Headers.Authorization =
            new AuthenticationHeaderValue("Bearer", token);
    });

    context.AddResponseTransform(async transformContext =>
    {
        if (transformContext.ProxyResponse?.StatusCode == HttpStatusCode.Unauthorized)
        {
            transformContext.HttpContext.Response.Headers["X-Auth-Hint"] =
                "Refresh your token";
        }
    });
});

这种方式适合简单的、与具体业务逻辑紧密耦合的场景。

实现 RequestTransform / ResponseTransform 类

对于可复用的逻辑,建议派生 RequestTransformResponseTransformResponseTrailersTransform 类:

csharp 复制代码
public sealed class TenantHeaderTransform : RequestTransform
{
    private readonly ITenantResolver _resolver;

    public TenantHeaderTransform(ITenantResolver resolver) => _resolver = resolver;

    public override async ValueTask ApplyAsync(RequestTransformContext context)
    {
        var tenant = await _resolver.ResolveAsync(context.HttpContext);
        if (tenant is not null)
        {
            context.ProxyRequest.Headers.Add("X-Tenant-Id", tenant.Id);
            context.ProxyRequest.Headers.Add("X-Tenant-Region", tenant.Region);
        }
    }
}

这种类化的写法天然支持依赖注入,便于单元测试,是大型项目推荐的方式。

ITransformProvider:跨路由统一接入

如果你希望在所有路由(或基于条件的多个路由)上统一注册一组 Transforms,实现 ITransformProvider 是更系统化的做法:

csharp 复制代码
public sealed class CorrelationIdTransformProvider : ITransformProvider
{
    public void ValidateRoute(TransformRouteValidationContext context) { }
    public void ValidateCluster(TransformClusterValidationContext context) { }

    public void Apply(TransformBuilderContext context)
    {
        context.AddRequestTransform(transformContext =>
        {
            var id = transformContext.HttpContext.TraceIdentifier;
            transformContext.ProxyRequest.Headers.Add("X-Correlation-Id", id);
            return ValueTask.CompletedTask;
        });
    }
}

// 注册
builder.Services.AddReverseProxy()
    .LoadFromConfig(...)
    .AddTransforms<CorrelationIdTransformProvider>();

ITransformProvider 还提供了 ValidateRouteValidateCluster 钩子,能够在配置加载阶段就发现非法配置,避免运行时才暴露问题。

ITransformFactory:扩展配置文件语法

最高级也最强大的扩展点是 ITransformFactory。通过它,你可以自定义配置文件中的 Transform 键名 ,让运营团队像使用内置 Transform 一样使用你的扩展。例如想要支持 { "GeoBlock": "CN,RU" } 这样的语法:

csharp 复制代码
public sealed class GeoBlockTransformFactory : ITransformFactory
{
    public bool Validate(TransformRouteValidationContext context,
                        IReadOnlyDictionary<string, string> values)
    {
        if (values.TryGetValue("GeoBlock", out var _)) return true;
        return false;
    }

    public bool Build(TransformBuilderContext context,
                     IReadOnlyDictionary<string, string> values)
    {
        if (!values.TryGetValue("GeoBlock", out var countries)) return false;
        var blocked = countries.Split(',', StringSplitOptions.RemoveEmptyEntries);
        context.AddRequestTransform(transformContext =>
        {
            // 实际项目中通过 GeoIP 服务解析
            var country = transformContext.HttpContext.Request.Headers["CF-IPCountry"]
                .FirstOrDefault();
            if (country is not null && blocked.Contains(country))
            {
                transformContext.HttpContext.Response.StatusCode = 403;
            }
            return ValueTask.CompletedTask;
        });
        return true;
    }
}

这种模式让 Transform 的能力扩展不必侵入业务代码,配置层与扩展实现解耦得非常彻底。

六、执行顺序与上下文

理解 Transforms 的执行顺序 对调试至关重要。YARP 按照配置中声明的先后顺序依次执行同一路由的所有请求 Transform,全部完成后才会发起对后端的请求;后端响应到达后,再按声明顺序执行响应 Transform。这意味着如果你先 PathRemovePrefixPathPattern,后者看到的路径已经是去掉前缀的版本,编排时务必谨记这一点。

RequestTransformContextResponseTransformContext 提供了三个关键属性:HttpContext(可访问 ASP.NET Core 的所有信息,包括路由值、用户身份、请求体)、ProxyRequest(即将发往后端的 HttpRequestMessage)和 ProxyResponse(仅响应阶段,从后端收到的 HttpResponseMessage)。一个常见错误是修改 HttpContext.Request.Path 期望影响后端请求路径------这是无效的,应该修改 transformContext.Path 这个专用属性,YARP 会在内部把它合成到 ProxyRequest 中。

七、性能与最佳实践

Transforms 在每个请求的关键路径上执行,性能敏感。几条经验值得遵循。第一,避免在 Transform 中做阻塞 IO,所有耗时操作都应是异步的,且要谨慎使用同步等待。第二,能在配置加载时完成的工作不要放到运行时,例如解析正则表达式应在 Transform 类的构造函数中完成,而非每次请求都重新编译。第三,对头部的操作要注意大小写不敏感性,且 IHeaderDictionaryHttpRequestHeaders 的 API 略有差异,前者属于 ASP.NET Core 入站请求,后者属于 HttpClient 出站请求,操作 ProxyRequest 时只能用后者。第四,谨慎使用 RequestHeadersCopy: false 这类彻底关闭头部复制的选项,它们会改变非常多默认行为,容易在生产中引发奇怪的问题。

八、调试与可观测性

排查 Transforms 问题最有效的工具是日志。YARP 的日志类别 Yarp.ReverseProxy.Forwarder.HttpForwarder 在 Debug 级别会输出请求和响应的关键信息,包括最终路径和头部。结合 Microsoft.AspNetCore.Routing 的日志可以看到路由匹配过程。生产环境中建议用 OpenTelemetry 接入 YARP 的 ActivitySource(Yarp.ReverseProxy),把代理过程作为分布式链路追踪的一部分,问题定位会快很多。

九、结语

YARP 的 Transforms 体系既提供了开箱即用的配置式 API,又开放了从委托到 Provider、再到 Factory 的多层次扩展点,构成了一个非常优雅的设计空间。对于初学者,从配置文件起步、组合使用内置 Transform 即可解决八成场景;对于复杂网关项目,则可以通过自定义 Transform 类、ITransformProvider 实现可测试、可复用的转换逻辑,再通过 ITransformFactory 把扩展暴露给配置层,让 DevOps 与开发者的边界清晰、协作顺畅。

掌握 Transforms 之后,你会发现 YARP 已经不再是一个"反向代理库",而更像是一个可编程的流量层框架------它的强大之处,正是从对每一个请求与响应字节的精确控制开始的。

相关推荐
Gopher_HBo1 小时前
负载均衡
后端
自由生长20242 小时前
RAG已死?什么标题党啊!
后端
东方小月2 小时前
5分钟搞懂Harness Engineering(驾驭工程):从提示词到AI Agent的进化之路
前端·后端·架构
折哥的程序人生 · 物流技术专研5 小时前
Java面试85题图解版(一):基础核心篇
java·开发语言·后端·面试
Moment5 小时前
面试官:如果产品经理给你多个需求,怎么让AI去完成❓❓❓
前端·后端·面试
每天进步一点_JL6 小时前
JVM 内存模型与 OOM 排查:从入门到实战
后端
REDcker7 小时前
个人博客网站建设指南 Markdown资产化与静态站选型部署
前端·后端·博客·markdown·网站·资产·建站
Supersist7 小时前
【设计模式03】使用模版模式+责任链模式优化实战
后端·设计模式·代码规范
Fox爱分享7 小时前
字节二面:10亿数据毫秒级查手机尾号后4位,答不出“异构索引”直接挂?
java·后端·面试