解决Asp.Net Core 3.1 中无法读取HttpContext.Request.Body的问题

需求

根据项目需要,要为WebApi实现一个ExceptionFilter,不仅要将WebApi执行过程中产生的异常信息进行收集,还要把WebApi的参数信息进行收集,以方便未来定位问题。

问题描述

对于WepApi的参数,一部分是通过URL获取,例如Get请求。对于Post或Put请求,表单数据是保存在Http请求的Body中的。基于此,我们可以在ExceptionFilter中,通过ExceptionContext参数,获取当前Http请求的Body数据。考虑到Body是Stream类型,读取方法如下:

public override async Task OnExceptionAsync(ExceptionContext context){
        var httpContext = context.HttpContext;
         var request = httpContext.Request;
         StreamReader sr = new StreamReader(request.Body);
         string body = await sr.ReadToEndAsync();
     }
 }

很遗憾,上面的代码读取到的Body数据为空。后来将代码移到ActionFilter,读取到的Body数据依然为空。最后将代码移到Middleware中,读取到的Body数据还是空。

问题解决

解决方案

结合Github和Stackflow类似问题的分析,得到解决方案如下,具体原因集分析请参看问题分析章节。

  1. 在Startup.cs中定义Middleware,设置缓存Http请求的Body数据。代码如下。自定义Middleware请放到Configure方法的最前面。
app.Use(next => new RequestDelegate(
          async context => {
              context.Request.EnableBuffering();
              await next(context);
          }
      )); 
  1. 在Filter或Middleware中,读取Body关键代码如下。
public override async Task OnExceptionAsync(ExceptionContext context){
        var httpContext = context.HttpContext;
         var request = httpContext.Request;
         request.Body.Position = 0;
         StreamReader sr = new StreamReader(request.Body);
         string body = await sr.ReadToEndAsync();
         request.Body.Position = 0;
     }
 }

注意事项

  1. Body在ASP.NET Core 的Http请求中是以Stream的形式存在。
  2. 首行Request.Position = 0,表示设定从Body流起始位置开始,读取整个Htttp请求的Body数据。
  3. 最后一行Request.Position = 0, 表示在读取到Body后,重新设置Stream到起始位置,方便后面的Filter或Middleware使用Body的数据。
  4. 在读取Body的时候,请尽量使用异步方式读取。ASP.NET Core默认是不支持同步读取的,会抛出异常,解决方法如下:
    1. Startup.cs文件中的ConfigureServices方法中添加以下代码
  services.Configure<KestrelServerOptions>(options =>
   {
       options.AllowSynchronousIO = true;
   });
  1. Startup.cs文件中,增加Using引用。
using Microsoft.AspNetCore.Server.Kestrel.Core;

异步处理(async/await)本来就是ASP.NET Core的重要特性,因此我也是推荐使用异步方式读取Body的Stream流中的数据。

问题分析

当前的解决方案,相比于最初始的代码,增加了两点:

  • EnableBuffering(HttpRequest)方法调用,该方法会将当前请求的Body数据缓存下来。
  • 在读取Http请求的Body流时候,设置从起始位置开始读取数据。

下面我们通过如下实验,来验证上述解决方案。我们的准备工作如下:

  • 准备一个Middleware,放到所有Middleware之前执行,读取Http Post请求的body。
  • 准备一个ActionFiler(异步),读取Http Post请求的body。
  • 准备一个ExceptionFilter(异步),读取Http Post请求的body。
  • 准备一个含有分母为0的异常的Action,该Action对应一个Post请求,含有一个Club类型参数,Club是一个对足球俱乐部的描述类。

实验1

我们在代码中,不调用EnableBuffering(HttpRequest)方法。因为不调用该扩展方法,Request.Position = 0这句会抛出异常如下,因此将该句也略去,完整代码以及Action参数设定请见附录实验1。

System.NotSupportedException: Specified method is not supported.
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpRequestStream.set_Position(Int64 value)
   at SportsNews.Web.Middlewares.ExceptionMiddleware.InvokeAsync(HttpContext httpContext) in D:\project\SportsNews\SportsNews.Web\Middlewares\ExceptionMiddleware.cs:line 33
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

实验结果:

控制台执行结果:
在这里插入图片描述
Postman返回结果:
在这里插入图片描述

实验结果分析

理论上代码执行路线应该是

Middleware -> Model Binding -> ActionExecuting Filter -> Action -> Exception Filter -> ActionExecuted Filter

从控制台的显示结果来看,Action Filter和Exception Filter的代码并没有被执行。分母为0的异常也并未抛出。

根据MS提供的ASP.NET Core Http 请求的流程和Postman的请求相应,显然,异常是在数据绑定阶段(Model Binding)抛出的。
在这里插入图片描述
原因就是在不执行EnableBuffering(HttpRequest)来缓存Body的情况下,Body只能被读取一次。

而这一次在我们定义的Middleware中已经使用了,所以在后面的数据绑定阶段(Model Binding),MVC的应用程序在从Body中读取数据,反序列化成具体的对象,作为Action的参数时候,读取失败了。因为此时Body中读取到数据为空,Postman显示解析的表单JSON数据失败。

实验2

在实验1的middleware中增加EnableBuffering(HttpRequest)的调用,但是在所有代码中读取Http请求的Body后,不重置Body流到起始位置,即不增加Request.Position = 0这句。

其他代码准备同实验1,完整代码以及Action参数设定请见附录实验2。

实验2的执行结果和实验1相同,控制台和Postman的返回结果同实验1完全相同,不再赘述。

实验结果分析

虽然我们缓存了Http请求中的Body,但是没有正确使用Body流,没有在代码中将Body流设置到起始位置,再进行读取。所以实验结果表现出来的还是Body只能读一次。

实验3

在实验2的基础上,每次读取完Http请求的Body后,增加Body流重置到初始位置的代码,具体代码参见附录实验3代码。

实验3基本符合我们的预期,除了ActionExecuting Filter没有读取到Body,其他Filter, Action和Middleware全部获取到Body数据,分母为0的异常已经抛出,具体如下:

控制台:
在这里插入图片描述
Postman:

在这里插入图片描述
为什么ActionExecuting Filter没有读取到Body没有读取到Body,根据MS提供的ASP.NET Core Http 请求的流程,我们的代码执行顺序应该是这样:

Middleware -> Model Binding -> ActionExecuting Filter -> Action -> Exception Filter -> ActionExecuted Filter

在我们自定义的Middleware中,我们使用完Body,进行了重置操作,所以Model Binding阶段没有出现实验1和2中出现的异常。但是Model Binding阶段MVC应用程序会读取请求的Body,但是读取完后,没有执行重置操作。所以 在ActionExecuting Filter中没有读到Body。

但是我们在ActionExecuting Filter中进行了重置操作,所以后面的Filter可以获取到Body。

基于此,所以我们文中开始时候的解决方案,重置操作时在读取Body前和读取Body后都做的。

对于在哪缓存Http请求的Body的问题,根据MS提供的如下Http请求流程图,我建议是放到所有的Middleware之前自定义Middleware并调用EnableBuffering(HttpRequest)方法,以保证后面的Middleware, Action或Filter都可以读取到Body。

在这里插入图片描述

附录

实验1代码

  • Action代码
 [CustomerActionFilter]
 [CustomerExceptionFilterAttribute]
 [HttpPost("checkerror/{Id:int}")]
 public IActionResult GetError2 ([FromBody] Club club) {
     var a = 1;
     var b = 2;
     var c = 3;
     var d = c / (b-a*2);
     return Ok (d);
 }
  • 参数Club的定义:
public class Club {
     public int Id { get; set; }
     public string Name { get; set; }
     public string City { get; set; }

     [Column (TypeName = "date")]
     public DateTime DateOfEstablishment { get; set; }
     public string History { get; set; }
     public League League { get; set; }
     public int LeagueId { get; set; }
}
  • Postman请求参数:

{
    "Id" : 10,
    "Name" : "Real Madrid",
    "City" : "Madrid",
    "History" : "Real Madrid has long history",
    "DateOfEstablishment" : "1902-03-06",
    "LeagueId":13
}
  • Middleware 代码:

 public class ExceptionMiddleware
    {
        public RequestDelegate _next { get; }
        public string body { get; private set; }
        public ExceptionMiddleware(RequestDelegate next)
        {
            this._next = next;          
        }
        public async Task InvokeAsync(HttpContext httpContext){
            var request = httpContext.Request;
            using (StreamReader reader = new StreamReader (request.Body, Encoding.UTF8, true, 1024, true)) {
                body = await reader.ReadToEndAsync();
                System.Console.WriteLine("This is ExceptionMiddleware. Body is " + body);
            }
            await _next(httpContext);        
        }
    }

  • Exception Filter的代码:
 public class CustomerExceptionFilter: ExceptionFilterAttribute
    {
        public CustomerExceptionService _exceptionService { get; }

        public CustomerExceptionFilter(
            CustomerExceptionService exceptionService,
            IHttpContextAccessor accessor){
            this._exceptionService = exceptionService 
                                    ?? throw new ArgumentNullException(nameof(exceptionService));
        }
        public override async Task OnExceptionAsync(ExceptionContext context){
            var httpContext = context.HttpContext;
            var request = httpContext.Request;
            StreamReader sr = new StreamReader(request.Body);
            string body = await sr.ReadToEndAsync();
            System.Console.WriteLine("This is OnExceptionAsync.");
            System.Console.WriteLine("Request body is " + body);
            if (!context.ExceptionHandled) {
                context.Result = new JsonResult(new {
                	Code = 501,
                	Msg = "Please contract Administrator."
                });
            }
        }
    }
    public class CustomerExceptionFilterAttribute : TypeFilterAttribute{
        public CustomerExceptionFilterAttribute (): base(typeof(CustomerExceptionFilter)){
        }
    }
  • Action Filter的代码:

public class CustomerActionFilterAttribute: ActionFilterAttribute
    {
         public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next){
            // before Action
            var httpContext = context.HttpContext;
            var request = httpContext.Request;
            StreamReader sr = new StreamReader(request.Body);
            string body = await sr.ReadToEndAsync();
            System.Console.WriteLine("This is OnActionExecuting.");
            System.Console.WriteLine("Request body is " + body);
            
            //Action            
            await next();

            // after Action
            //request.Body.Position = 0; 
            StreamReader sr2 = new StreamReader(request.Body);
            body = await sr2.ReadToEndAsync();
            System.Console.WriteLine("This is OnActionExecuted.");
            System.Console.WriteLine("Request body is " + body);
          //  request.Body.Position = 0;          
        } 
    }

实验2代码

  • Middleware代码:

public class ExceptionMiddleware
{
     public RequestDelegate _next { get; }
     public string body { get; private set; }
     public ExceptionMiddleware(RequestDelegate next)
     {
         this._next = next;          
     }
     public async Task InvokeAsync(HttpContext httpContext){
         var request = httpContext.Request;
         request.EnableBuffering();
         StreamReader reader = new StreamReader (request.Body) ;
         string body = await reader.ReadToEndAsync();  
         System.Console.WriteLine("This is ExceptionMiddleware. Body is " + body);
         await _next(httpContext);      
     }
 }

实验3代码

  • Middleware 代码:

 public class ExceptionMiddleware
{
     public RequestDelegate _next { get; }
     public string body { get; private set; }
     public ExceptionMiddleware(RequestDelegate next)
     {
         this._next = next;          
     }
     public async Task InvokeAsync(HttpContext httpContext){
         var request = httpContext.Request;
         request.EnableBuffering();
         StreamReader reader = new StreamReader (request.Body) ;
         string body = await reader.ReadToEndAsync(); 
         request.Body.Position = 0; 
         System.Console.WriteLine("This is ExceptionMiddleware. Body is " + body);
         await _next(httpContext);      
     }
 }

  • Exception Filter的代码:
 public class CustomerExceptionFilter: ExceptionFilterAttribute
    {
        public CustomerExceptionService _exceptionService { get; }

        public CustomerExceptionFilter(
            CustomerExceptionService exceptionService,
            IHttpContextAccessor accessor){
            this._exceptionService = exceptionService 
                                    ?? throw new ArgumentNullException(nameof(exceptionService));
        }
        public override async Task OnExceptionAsync(ExceptionContext context){
            var httpContext = context.HttpContext;
            var request = httpContext.Request;
            StreamReader sr = new StreamReader(request.Body);
            string body = await sr.ReadToEndAsync();
            request.Body.Position = 0;
            System.Console.WriteLine("This is OnExceptionAsync.");
            System.Console.WriteLine("Request body is " + body);
            if (!context.ExceptionHandled) {
                context.Result = new JsonResult(new {
                	Code = 501,
                	Msg = "Please contract Administrator."
                });
            }
        }
    }
    public class CustomerExceptionFilterAttribute : TypeFilterAttribute{
        public CustomerExceptionFilterAttribute (): base(typeof(CustomerExceptionFilter)){
        }
    }
  • Action Filter的代码:

public class CustomerActionFilterAttribute: ActionFilterAttribute
    {
         public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next){
            // before Action
            var httpContext = context.HttpContext;
            var request = httpContext.Request;
            StreamReader sr = new StreamReader(request.Body);
            string body = await sr.ReadToEndAsync();
            request.Body.Position = 0;
            System.Console.WriteLine("This is OnActionExecuting.");
            System.Console.WriteLine("Request body is " + body);
            
            //Action            
            await next();

            // after Action
            //request.Body.Position = 0; 
            StreamReader sr2 = new StreamReader(request.Body);
            body = await sr2.ReadToEndAsync();
            request.Body.Position = 0;
            System.Console.WriteLine("This is OnActionExecuted.");
            System.Console.WriteLine("Request body is " + body);
          //  request.Body.Position = 0;          
        } 
    }
  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
在 .NET Core 3.1 ,可以使用过滤器来验证 Session。具体实现步骤如下: 1. 创建一个 Session 验证过滤器类,继承自 ActionFilterAttribute: ```csharp public class SessionValidationFilter : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { var session = context.HttpContext.Session.GetString("UserName"); if (string.IsNullOrEmpty(session)) { context.Result = new RedirectToActionResult("Index", "Login", null); } } } ``` 2. 在需要验证 Session 的 Controller 或 Action 上,添加该过滤器的特性: ```csharp [SessionValidationFilter] public class HomeController : Controller { //... } ``` 这样,当请求到达该 Controller 或 Action 时,就会先执行 SessionValidationFilter 过滤器的 OnActionExecuting 方法。在该方法,我们通过 HttpContext.Session.GetString("UserName") 方法获取 Session 的 UserName 值,如果为空则重定向到登录页面。如果验证通过,则会继续执行后续的 Action。 需要注意的是,使用 Session 验证过滤器需要先在 Startup.cs 启用 Session,添加以下代码: ```csharp public void ConfigureServices(IServiceCollection services) { //... services.AddDistributedMemoryCache(); services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(30); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; }); //... } ``` 这样就可以在 .NET Core 3.1 实现 Session 验证过滤器了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值