介绍
代理请求时,通常修改请求或响应的各个部分,以适应目标服务器的要求或流出其他数据,例如客户端的原始 IP 地址。 此过程通过Transforms实现。 为应用程序全局定义转换类型,然后各个路由提供参数以启用和配置这些转换。 原始请求对象不会由这些转换修改,只修改代理请求。
YARP 包括一组可供使用的内置请求和响应转换。 有关详细信息,请参阅 YARP 请求和响应转换。 如果这些转换不够,则可以添加自定义转换。
RequestTransform
所有请求转换都必须派生自抽象基类 RequestTransform。 这些可以自由修改代理 HttpRequestMessage。 避免读取或修改请求正文,因为这可能会中断代理流。 另请考虑为 TransformBuilderContext 添加参数化扩展方法,以提高可发现性和易用性。
请求转换可能会有条件地生成即时响应,例如错误条件。 这样可以防止运行任何剩余的转换并防止代理请求。 这是通过将 HttpResponse.StatusCode 设置为不等于200的值,调用 HttpResponse.StartAsync(),或者写入 HttpResponse.Body 或 BodyWriter 来指示。
AddRequestTransform 是一种 TransformBuilderContext 扩展方法,它将请求转换定义为 Func<RequestTransformContext, ValueTask>。 这样就可以创建自定义请求转换,而无需实现 RequestTransform 派生类。
ResponseTransform
所有响应转换都必须派生自抽象基类 ResponseTransform。 这些可以自由修改客户端 HttpResponse。 避免读取或修改响应正文,因为这可能会中断代理流。 另请考虑添加参数化扩展方法 TransformBuilderContext ,以便于可发现性和易于使用。
AddResponseTransform 是一种 TransformBuilderContext 扩展方法,能够将响应转换表述为 Func<ResponseTransformContext, ValueTask>。 这样就可以创建自定义响应转换,而无需实现 ResponseTransform 派生类。
ResponseTrailersTransform
所有响应尾部转换器都必须派生自抽象基类 ResponseTrailersTransform。 这些可以自由修改客户端 HttpResponse 尾部。 这些程序在响应正文之后执行,不应尝试修改响应标头或正文。 另请考虑添加参数化扩展方法 TransformBuilderContext ,以便于可发现性和易于使用。
AddResponseTrailersTransform 是一种 TransformBuilderContext 扩展方法,用于将响应预告片转换定义为 . Func<ResponseTrailersTransformContext, ValueTask>. 这允许创建自定义响应尾部转换,而无需实现 ResponseTrailersTransform 派生类。
请求正文转换
YARP 不提供任何用于修改请求正文的内置转换。 但是,可以通过自定义转换来修改正文。
请注意修改了哪些类型的请求、数据缓存量、实施超时、解析不受信任的输入,以及更新与正文相关的标头,如 Content-Length。
下面的示例使用简单、低效的缓冲来转换请求。 更高效的实现将把 HttpContext.Request.Body 包装并替换为一个流,该流在数据从客户端代理到服务器时执行所需的修改。 这还需要删除 Content-Length 头,因为无法提前知道最终长度。
此示例需要 YARP 1.1,请参阅 https://github.com/microsoft/reverse-proxy/pull/1569。
.AddTransforms(context =>
{
context.AddRequestTransform(async requestContext =>
{
using var reader =
new StreamReader(requestContext.HttpContext.Request.Body);
// TODO: size limits, timeouts
var body = await reader.ReadToEndAsync();
if (!string.IsNullOrEmpty(body))
{
body = body.Replace("Alpha", "Charlie");
var bytes = Encoding.UTF8.GetBytes(body);
// Change Content-Length to match the modified body, or remove it
requestContext.HttpContext.Request.Body = new MemoryStream(bytes);
// Request headers are copied before transforms are invoked, update any
// needed headers on the ProxyRequest
requestContext.ProxyRequest.Content.Headers.ContentLength =
bytes.Length;
}
});
});
自定义转换只能修改请求正文(如果已存在)。 它们无法向没有正文的请求中添加新的正文(例如,没有正文的 POST 请求或 GET 请求)。 如果需要为特定的 HTTP 方法和路由添加正文,您必须在 YARP 之前运行的中间件中完成此操作,而不是在转换中进行。
以下中间件演示如何向没有正文的请求添加正文:
public class AddRequestBodyMiddleware
{
private readonly RequestDelegate _next;
public AddRequestBodyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Only modify specific route and method
if (context.Request.Method == HttpMethods.Get &&
context.Request.Path == "/special-route")
{
var bodyContent = "key=value";
var bodyBytes = Encoding.UTF8.GetBytes(bodyContent);
// Create a new request body
context.Request.Body = new MemoryStream(bodyBytes);
context.Request.ContentLength = bodyBytes.Length;
// Replace IHttpRequestBodyDetectionFeature so YARP knows
// a body is present
context.Features.Set<IHttpRequestBodyDetectionFeature>(
new CustomBodyDetectionFeature());
}
await _next(context);
}
// Helper class to indicate the request can have a body
private class CustomBodyDetectionFeature : IHttpRequestBodyDetectionFeature
{
public bool CanHaveBody => true;
}
}
注释
在中间件中,可以通过 context.GetRouteModel().Config.RouteId 为特定的 YARP 路由有条件地应用此逻辑。
响应正文转换
YARP 不提供任何用于修改响应正文的内置转换。 但是,可以通过自定义转换来修改正文。
请注意修改了哪些类型的响应、数据缓存量、实施超时、解析不受信任的输入,以及更新与正文相关的标头,如 Content-Length。 在修改内容之前,可能需要解压缩内容,如 Content-Encoding 标头所指示,然后重新压缩或删除标头。
下面的示例使用简单、低效的缓冲来转换响应。 更高效的实现会将 ReadAsStreamAsync() 返回的流与执行所需修改的流整合在一起,因为数据会从客户端代理到服务器。 这还需要删除 Content-Length 头,因为无法提前知道最终长度。
.AddTransforms(context =>
{
context.AddResponseTransform(async responseContext =>
{
var stream =
await responseContext.ProxyResponse.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
// TODO: size limits, timeouts
var body = await reader.ReadToEndAsync();
if (!string.IsNullOrEmpty(body))
{
responseContext.SuppressResponseBody = true;
body = body.Replace("Bravo", "Charlie");
var bytes = Encoding.UTF8.GetBytes(body);
// Change Content-Length to match the modified body, or remove it
responseContext.HttpContext.Response.ContentLength = bytes.Length;
// Response headers are copied before transforms are invoked, update
// any needed headers on the HttpContext.Response
await responseContext.HttpContext.Response.Body.WriteAsync(bytes);
}
});
});
ITransformProvider
ITransformProvider 提供上述描述的 AddTransforms 功能,以及 DI 集成和验证支持。
通过调用 ITransformProvider 可以在 DI 中注册 AddTransforms。 可以注册多个 ITransformProvider 实现,所有实现都将运行。
ITransformProvider 有两种方法,Validate 和 Apply。
Validate 让你有机会检查配置转换(如自定义元数据)所需的任何参数的路由,并返回上下文中的验证错误(如果需要的值缺失或无效)。
Apply 方法提供的功能与上述 AddTransform 方法提供的功能相同,并且此方法会按路由添加和配置转换。
services.AddReverseProxy()
.LoadFromConfig(_configuration.GetSection("ReverseProxy"))
.AddTransforms<MyTransformProvider>();
internal class MyTransformProvider : ITransformProvider
{
public void ValidateRoute(TransformRouteValidationContext context)
{
// Check all routes for a custom property and validate the associated
// transform data
if (context.Route.Metadata?.TryGetValue("CustomMetadata", out var value) ??
false)
{
if (string.IsNullOrEmpty(value))
{
context.Errors.Add(new ArgumentException(
"A non-empty CustomMetadata value is required"));
}
}
}
public void ValidateCluster(TransformClusterValidationContext context)
{
// Check all clusters for a custom property and validate the associated
// transform data.
if (context.Cluster.Metadata?.TryGetValue("CustomMetadata", out var value)
?? false)
{
if (string.IsNullOrEmpty(value))
{
context.Errors.Add(new ArgumentException(
"A non-empty CustomMetadata value is required"));
}
}
}
public void Apply(TransformBuilderContext transformBuildContext)
{
// Check all routes for a custom property and add the associated transform.
if ((transformBuildContext.Route.Metadata?.TryGetValue("CustomMetadata",
out var value) ?? false)
|| (transformBuildContext.Cluster?.Metadata?.TryGetValue(
"CustomMetadata", out value) ?? false))
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException(
"A non-empty CustomMetadata value is required");
}
transformBuildContext.AddRequestTransform(transformContext =>
{
transformContext.ProxyRequest.Options.Set(
new HttpRequestOptionsKey<string>("CustomMetadata"), value);
return default;
});
}
}
}
ITransformFactory
想要将其自定义转换与 Transforms 配置部分集成的开发人员可以实现一个 ITransformFactory。 这应该使用 AddTransformFactory<T>() 方法在 DI 中进行注册。 可以注册多个工厂,而且所有工厂都会被使用。
ITransformFactory 提供两种方法,分别是 Validate 和 Build。 这些过程一次处理一组转换值,以 IReadOnlyDictionary<string, string> 表示。
加载配置时调用Validate方法以验证内容并报告所有错误。 任何报告的错误都会阻止应用配置。
该方法 Build 采用给定的配置,并为路由生成关联的转换实例。
services.AddReverseProxy()
.LoadFromConfig(_configuration.GetSection("ReverseProxy"))
.AddTransformFactory<MyTransformFactory>();
internal class MyTransformFactory : ITransformFactory
{
public bool Validate(TransformRouteValidationContext context,
IReadOnlyDictionary<string, string> transformValues)
{
if (transformValues.TryGetValue("CustomTransform", out var value))
{
if (string.IsNullOrEmpty(value))
{
context.Errors.Add(new ArgumentException(
"A non-empty CustomTransform value is required"));
}
return true; // Matched
}
return false;
}
public bool Build(TransformBuilderContext context,
IReadOnlyDictionary<string, string> transformValues)
{
if (transformValues.TryGetValue("CustomTransform", out var value))
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException(
"A non-empty CustomTransform value is required");
}
context.AddRequestTransform(transformContext =>
{
transformContext.ProxyRequest.Options.Set(
new HttpRequestOptionsKey<string>("CustomTransform"), value);
return default;
});
return true;
}
return false;
}
}
Validate 和 Build 如果已将给定的转换配置标识为它们拥有的配置,则它们会返回 true。 A ITransformFactory 可以实现多个转换。 任何未被任何 RouteConfig.Transforms 处理的 ITransformFactory 条目都会被视为配置错误,并阻止配置生效。
考虑在RouteConfig上添加参数化扩展方法,例如WithTransformQueryValue,以便于程序化路由构建。
public static RouteConfig WithTransformQueryValue(this RouteConfig routeConfig,
string queryKey, string value, bool append = true)
{
var type = append ? QueryTransformFactory.AppendKey :
QueryTransformFactory.SetKey;
return routeConfig.WithTransform(transform =>
{
transform[QueryTransformFactory.QueryValueParameterKey] = queryKey;
transform[type] = value;
});
}