【问题标题】:Modify middleware response修改中间件响应
【发布时间】:2017-11-14 10:29:11
【问题描述】:

我的要求:编写一个中间件,从来自另一个后续中间件(例如 Mvc)的响应中过滤掉所有“坏词”。

问题:响应流。因此,当我们从已经写入响应的后续中间件返回FilterBadWordsMiddleware 时,我们为时已晚...因为响应已经开始发送,这导致了众所周知的错误response has already started...

既然这是许多不同情况下的要求——如何处理呢?

【问题讨论】:

    标签: asp.net-core .net-core asp.net-core-middleware


    【解决方案1】:

    将响应流替换为MemoryStream 以防止其发送。修改响应后返回原始流:

        public async Task Invoke(HttpContext context)
        {
            bool modifyResponse = true;
            Stream originBody = null;
    
            if (modifyResponse)
            {
                //uncomment this line only if you need to read context.Request.Body stream
                //context.Request.EnableRewind();
    
                originBody = ReplaceBody(context.Response);
            }
    
            await _next(context);
    
            if (modifyResponse)
            {
                //as we replaced the Response.Body with a MemoryStream instance before,
                //here we can read/write Response.Body
                //containing the data written by middlewares down the pipeline 
    
                //finally, write modified data to originBody and set it back as Response.Body value
                ReturnBody(context.Response, originBody);
            }
        }
    
        private Stream ReplaceBody(HttpResponse response)
        {
            var originBody = response.Body;
            response.Body = new MemoryStream();
            return originBody;
        }
    
        private void ReturnBody(HttpResponse response, Stream originBody)
        {
            response.Body.Seek(0, SeekOrigin.Begin);
            response.Body.CopyTo(originBody);
            response.Body = originBody;
        }
    

    这是一种解决方法,它可能会导致性能问题。我希望在这里看到更好的解决方案。

    【讨论】:

    • 这行得通,谢谢!我发现在某些情况下,将回调附加到 context.Response.OnStarting() 也可以,但在修改响应时却不行。另外我不喜欢使用OnStarting(),因为它破坏了迭代中间件工作流程。
    • 为了将来参考,在写入响应之前,设置内容长度属性会有所帮助。
    • 在 Dotnet Core 3 中,我得到“System.InvalidOperationException: Response Content-Length mismatch”来解决这个问题,我添加了 context.Response.ContentLength = json.Length;
    【解决方案2】:

    很遗憾,由于我的分数太低,我无法发表评论。 所以只是想发布我对优秀顶级解决方案的扩展,以及对 .NET Core 3.0+ 的修改

    首先

    context.Request.EnableRewind();
    

    已改为

    context.Request.EnableBuffering();
    

    在 .NET Core 3.0+ 中

    这是我读/写正文内容的方式:

    首先是一个过滤器,所以我们只需要修改我们感兴趣的内容类型

    private static readonly IEnumerable<string> validContentTypes = new HashSet<string>() { "text/html", "application/json", "application/javascript" };
    

    这是一种将 [[[Translate me]]] 等隐含文本转换为其翻译的解决方案。这样我就可以标记所有需要翻译的东西,阅读我们从翻译器那里得到的 po-file,然后在输出流中进行翻译替换——不管掘金文本是否在剃刀视图中,javascript管他呢。 有点像 TurquoiseOwl i18n 包,但在 .NET Core 中,不幸的是,这个优秀的包不支持。

    ...
    
    if (modifyResponse)
    {
        //as we replaced the Response.Body with a MemoryStream instance before,
        //here we can read/write Response.Body
        //containing the data written by middlewares down the pipeline
    
        var contentType = context.Response.ContentType?.ToLower();
        contentType = contentType?.Split(';', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();   // Filter out text/html from "text/html; charset=utf-8"
    
        if (validContentTypes.Contains(contentType))
        {
            using (var streamReader = new StreamReader(context.Response.Body))
            {
                // Read the body
                context.Response.Body.Seek(0, SeekOrigin.Begin);
                var responseBody = await streamReader.ReadToEndAsync();
    
                // Replace [[[Bananas]]] with translated texts - or Bananas if a translation is missing
                responseBody = NuggetReplacer.ReplaceNuggets(poCatalog, responseBody);
    
                // Create a new stream with the modified body, and reset the content length to match the new stream
                var requestContent = new StringContent(responseBody, Encoding.UTF8, contentType);
                context.Response.Body = await requestContent.ReadAsStreamAsync();//modified stream
                context.Response.ContentLength = context.Response.Body.Length;
            }
        }
    
        //finally, write modified data to originBody and set it back as Response.Body value
        ReturnBody(context.Response, originBody);
    }
    ...
    
    private void ReturnBody(HttpResponse response, Stream originBody)
    {
        response.Body.Seek(0, SeekOrigin.Begin);
        response.Body.CopyToAsync(originBody);
        response.Body = originBody;
    }
    

    【讨论】:

    • ReturnBody 是做什么的?
    • 对不起,在解决方案建议中添加了 RetunBody 功能
    【解决方案3】:

    基于我使用的代码的更简单的版本:

    /// <summary>
    /// The middleware Invoke method.
    /// </summary>
    /// <param name="httpContext">The current <see cref="HttpContext"/>.</param>
    /// <returns>A Task to support async calls.</returns>
    public async Task Invoke(HttpContext httpContext)
    {
        var originBody = httpContext.Response.Body;
        try
        {
            var memStream = new MemoryStream();
            httpContext.Response.Body = memStream;
    
            await _next(httpContext).ConfigureAwait(false);
    
            memStream.Position = 0;
            var responseBody = new StreamReader(memStream).ReadToEnd();
    
            //Custom logic to modify response
            responseBody = responseBody.Replace("hello", "hi", StringComparison.InvariantCultureIgnoreCase);
    
            var memoryStreamModified = new MemoryStream();
            var sw = new StreamWriter(memoryStreamModified);
            sw.Write(responseBody);
            sw.Flush();
            memoryStreamModified.Position = 0;
    
            await memoryStreamModified.CopyToAsync(originBody).ConfigureAwait(false);
        }
        finally
        {
            httpContext.Response.Body = originBody;
        }
    }
    

    【讨论】:

    • 不幸的是我收到了System.InvalidOperationException: Response Content-Length mismatch: too few bytes written.
    【解决方案4】:

    “真实”的生产场景可以在这里找到:tethys logging middeware

    如果你按照链接中给出的逻辑,不要忘记在调用_next(httpContext)之前添加httpContext.Request.EnableRewind()Microsoft.AspNetCore.Http.Internal命名空间的扩展方法)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-08-03
      • 2017-01-12
      • 1970-01-01
      • 2010-12-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多