ASP.NET 核心响应正文的前缀文本

Prefix text to ASP.NET Core response body

我正在尝试将字符串 )]}',\n 添加到任何 JSON 的响应正文中。我认为 IAsyncResultFilter 是我需要使用的,但我运气不好。如果我使用下面的代码,它 附加 文本到响应,因为调用 await next() 写入响应管道。不过,如果我尝试查看之前的上下文,我无法判断响应实际上是什么以了解它是否是 JSON.

public class JsonPrefixFilter : IAsyncResultFilter
{
    public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        var executed = await next();
        var response = executed.HttpContext.Response;
        if (response.ContentType == null || !response.ContentType.StartsWith("application/json"))
            return;

        var prefix = Encoding.UTF8.GetBytes(")]}',\n");
        var bytes = new ReadOnlyMemory<byte>(prefix);
        await response.BodyWriter.WriteAsync(bytes);
    }
}

您可以在蒸汽机上使用 Seek 来倒带它。问题是,您只能继续添加到默认 HttpResponseStream,它不支持搜索。 因此,您可以使用 中的技术并暂时将其替换为 MemoryStream:

private Stream ReplaceBody(HttpResponse response)
{
    var originBody = response.Body;
    response.Body = new MemoryStream();
    return originBody;
}
private async Task ReturnBodyAsync(HttpResponse response, Stream originalBody)
{
    response.Body.Seek(0, SeekOrigin.Begin);
    await response.Body.CopyToAsync(originalBody);
    response.Body = originalBody;
}
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
    var originalBody = ReplaceBody(context.HttpContext.Response); // replace the default stream with MemoryStream

    await next(); // we probably dont care about the return of this call. it's all in the context
    var response = context.HttpContext.Response;
    if (response.ContentType == null || !response.ContentType.StartsWith("application/json"))
        return;

    var prefix = Encoding.UTF8.GetBytes(")]}',\n");
    var bytes = new ReadOnlyMemory<byte>(prefix);
    response.Body.Seek(0, SeekOrigin.Begin); // now you can seek. but you will notice that it overwrites the response so you might need to make extra space in the buffer
    await response.BodyWriter.WriteAsync(bytes);

    await ReturnBodyAsync(context.HttpContext.Response, originalBody); // revert the reference, copy data into default stream and return it
}

由于您需要恢复对原始流的引用,因此这变得更加复杂,因此您必须小心处理。

有更多的上下文。

感谢 timur 的 post 我能够想出这个可行的解决方案。

public class JsonPrefixFilter : IAsyncResultFilter
{
    public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        var response = context.HttpContext.Response;

        // ASP.NET Core will always send the contents of the original Body stream back to the client.
        var originalBody = response.Body;

        // We want to write into a memory stream instead of the actual response body for now.
        var ms = new MemoryStream();
        response.Body = ms;

        // After this call the body is written into the memory stream and the properties
        // of the response object are populated.
        await next();

        if (response.ContentType != null && response.ContentType.StartsWith("application/json")) {
            var prefix = Encoding.UTF8.GetBytes(")]}',\n");

            var prefixMemoryStream = new MemoryStream();
            await prefixMemoryStream.WriteAsync(prefix);
            await prefixMemoryStream.WriteAsync(ms.ToArray());
            prefixMemoryStream.Seek(0, SeekOrigin.Begin);

            // Now put the stream back that .NET wants to use and copy the memory stream to it.
            response.Body = originalBody;
            await prefixMemoryStream.CopyToAsync(response.Body);
        } else {
            // If it's not JSON, don't muck with the stream, so just put things back.
            response.Body = originalBody;
            ms.Seek(0, SeekOrigin.Begin);
            await ms.CopyToAsync(response.Body);
        }
    }
}

更新:

我一直不喜欢上面的方法,所以我改用了这个解决方案。我没有调用 AddJsonOptions,而是从 ASP.NET 的格式化程序中获得灵感来使用它:

public class XssJsonOutputFormatter : TextOutputFormatter
{
    private static readonly byte[] XssPrefix = Encoding.UTF8.GetBytes(")]}',\n");

    public JsonSerializerOptions SerializerOptions { get; }

    public XssJsonOutputFormatter()
    {
        SerializerOptions = new() {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
            ReferenceHandler = ReferenceHandler.IgnoreCycles
        };

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
    }

    public override sealed async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        ArgumentNullException.ThrowIfNull(context, nameof(context));
        ArgumentNullException.ThrowIfNull(selectedEncoding, nameof(selectedEncoding));

        var httpContext = context.HttpContext;
        var objectType = context.Object?.GetType() ?? context.ObjectType ?? typeof(object);

        var responseStream = httpContext.Response.Body;
        try {
            await responseStream.WriteAsync(XssPrefix);
            await JsonSerializer.SerializeAsync(responseStream, context.Object, objectType, SerializerOptions, httpContext.RequestAborted);
            await responseStream.FlushAsync(httpContext.RequestAborted);
        } catch (OperationCanceledException) when (context.HttpContext.RequestAborted.IsCancellationRequested) {
        }
    }
}

现在,当您调用 .AddControllers() 时,您只需将其设置为第一个输出格式化程序:

services.AddControllers(options => {
    options.Filters.Add(new ProducesAttribute("application/json"));
    options.OutputFormatters.Insert(0, new XssJsonOutputFormatter());
});

显然,您可以改进它以在构造函数中采用序列化选项,但我的所有项目都将像上面那样工作,所以我只是将其硬编码在其中。