介紹
代理請求時,通常會修改請求或回應的部分內容,以符合目的伺服器的需求,或傳遞其他數據,例如客戶端的原始 IP 地址。 此流程是透過轉換來執行。 轉換類型會針對應用程式全域定義,然後個別路由提供參數來啟用和設定這些轉換。 原始要求物件不會透過這些轉換被修改,只有代理要求會被修改。
YARP 包含一組可用於的內建要求和響應轉換。 如需詳細資訊,請參閱 YARP 要求和響應轉換。 如果這些轉換不足,則可以新增自定義轉換。
RequestTransform
所有要求轉換都必須衍生自抽象基類 RequestTransform。 這些可以自由修改 proxy HttpRequestMessage。 避免讀取或修改請求主體,因為這可能會中斷代理流程。 也請考慮在 上 TransformBuilderContext 新增參數化擴充方法,以方便探索和使用。
要求轉換可能會有條件地產生立即回應,例如在發生錯誤狀況時。 這樣做可以防止執行任何剩餘的轉換程序,並防止要求通過代理。 這是藉由將 HttpResponse.StatusCode 設定為 200 以外的值,或呼叫 HttpResponse.StartAsync(),或寫入至 HttpResponse.Body 或 BodyWriter來表示。
AddRequestTransform 是一個 TransformBuilderContext 擴充方法,將請求轉換定義為 Func<RequestTransformContext, ValueTask>。 這可讓您建立自定義要求轉換,而不實作衍生類別 RequestTransform。
ResponseTransform
所有回應轉換都必須衍生自抽象基類 ResponseTransform。 這些可以自由修改用戶端 HttpResponse。 避免讀取或修改回應主體,因為這可能會中斷 Proxy 流程。 也請考慮在 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 之前運行的中介軟體 middleware 中執行,而不是在轉換中進行。
下列中間件示範如何將內容新增至沒有內容的請求:
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將 AddTransforms 在 DI 中註冊。 您可以註冊多個 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。
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;
});
}