.net core MVC 通过 Filters 过滤器拦截请求及响应内容
前提:
需要nuget Microsoft.Extensions.Logging.Log4Net.AspNetCore 2.2.6;
Swashbuckle.AspNetCore 我暂时用的是 4.01;
描述:通过 Filters 拦截器获取 Api 请求内容及响应内容,并记录到日志文件;
有文中代码记录接口每次请求及响应情况如下图:
解决办法:
步骤1 配置 Swagger 接口文档
对startup.cs 进行修改代码如下:
ConfigureServices 中增加Swagger 配置
services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Version = "v1", Title = "Filters 过滤器测试Api", Description = @"通过 IActionFilter, IAsyncResourceFilter 拦截器拦截请求及响应上下文并记录到log4日志" }); c.IncludeXmlComments(this.GetType().Assembly.Location.Replace(".dll", ".xml"), true); //是需要设置 XML 注释文件的完整路径 });
对 Configure Http管道增加 SwaggerUi
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseMvc(); app.UseSwagger(); app.UseSwaggerUI(o => { o.SwaggerEndpoint("/swagger/v1/swagger.json", "Filters 过滤器测试Api"); }); }
步骤2 创建Log4net 日志帮助类 LogHelper.cs
/// <summary> /// 日志帮助类 /// </summary> public static class LogHelper { /// <summary> /// 日志提供者 /// </summary> private static ILogger logger; /// <summary> /// 静太方法构造函数 /// </summary> static LogHelper() { logger = new LoggerFactory().AddConsole().AddDebug().AddLog4Net().CreateLogger("Logs"); } /// <summary> /// 打印提示 /// </summary> /// <param name="message">日志内容</param> public static void Info(object message) { logger.LogInformation(message?.ToString()); } /// <summary> /// 打印错误 /// </summary> /// <param name="message">日志内容</param> public static void Error(object message) { logger.LogError(message?.ToString()); } /// <summary> /// 打印错误 /// </summary> /// <param name="ex">异常信息</param> /// <param name="message">日志内容</param> public static void Error(Exception ex, string message) { logger.LogError(ex, message); } /// <summary> /// 调试信息打印 /// </summary> /// <param name="message"></param> public static void Debug(object message) { logger.LogDebug(message?.ToString()); } }
步骤3 定义可读写的Http 上下文流接口 IReadableBody.cs 及 http 请求上下文中间件 HttpContextMiddleware.cs
/// <summary> /// 定义可读Body的接口 /// </summary> public interface IReadableBody { /// <summary> /// 获取或设置是否可读 /// </summary> bool IsRead { get; set; } /// <summary> /// 读取文本内容 /// </summary> /// <returns></returns> Task<string> ReadAsStringAsync(); }
/// <summary> /// Http 请求中间件 /// </summary> public class HttpContextMiddleware { /// <summary> /// 处理HTTP请求 /// </summary> private readonly RequestDelegate next; /// <summary> /// 构造 Http 请求中间件 /// </summary> /// <param name="next"></param> public HttpContextMiddleware(RequestDelegate next) { this.next = next; } /// <summary> /// 执行响应流指向新对象 /// </summary> /// <param name="context"></param> /// <returns></returns> public Task Invoke(HttpContext context) { context.Response.Body = new ReadableResponseBody(context.Response.Body); return this.next.Invoke(context); } /// <summary> /// 可读的Response Body /// </summary> private class ReadableResponseBody : MemoryStream, IReadableBody { /// <summary> /// 流内容 /// </summary> private readonly Stream body; /// <summary> /// 获取或设置是否可读 /// </summary> public bool IsRead { get; set; } /// <summary> /// 构造自定义流 /// </summary> /// <param name="body"></param> public ReadableResponseBody(Stream body) { this.body = body; } /// <summary> /// 写入响应流 /// </summary> /// <param name="buffer"></param> /// <param name="offset"></param> /// <param name="count"></param> public override void Write(byte[] buffer, int offset, int count) { this.body.Write(buffer, offset, count); if (this.IsRead) { base.Write(buffer, offset, count); } } /// <summary> /// 写入响应流 /// </summary> /// <param name="source"></param> public override void Write(ReadOnlySpan<byte> source) { this.body.Write(source); if (this.IsRead) { base.Write(source); } } /// <summary> /// 刷新响应流 /// </summary> public override void Flush() { this.body.Flush(); if (this.IsRead) { base.Flush(); } } /// <summary> /// 读取响应内容 /// </summary> /// <returns></returns> public Task<string> ReadAsStringAsync() { if (this.IsRead == false) { throw new NotSupportedException(); } this.Seek(0, SeekOrigin.Begin); using (var reader = new StreamReader(this)) { return reader.ReadToEndAsync(); } } protected override void Dispose(bool disposing) { this.body.Dispose(); base.Dispose(disposing); } } }
步骤4 配置Http管通增加 Http请求上下文中件间
打开 Startup.cs ,对管通 Configure 增加如下中间件代码:
app.UseMiddleware<HttpContextMiddleware>();
步骤5 增加Api 过滤器 ApiFilterAttribute
/// <summary> /// Api 过滤器,记录请求上下文及响应上下文 /// </summary> public class ApiFilterAttribute : Attribute, IActionFilter, IAsyncResourceFilter { /// <summary> /// 请求Api 资源时 /// </summary> /// <param name="context"></param> /// <param name="next"></param> /// <returns></returns> public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { // 执行前 try { await next.Invoke(); } catch { } // 执行后 await OnResourceExecutedAsync(context); } /// <summary> /// 记录Http请求上下文 /// </summary> /// <param name="context"></param> /// <returns></returns> public async Task OnResourceExecutedAsync(ResourceExecutingContext context) { var log = new HttpContextMessage { RequestMethod = context.HttpContext.Request.Method, ResponseStatusCode = context.HttpContext.Response.StatusCode, RequestQurey = context.HttpContext.Request.QueryString.ToString(), RequestContextType = context.HttpContext.Request.ContentType, RequestHost = context.HttpContext.Request.Host.ToString(), RequestPath = context.HttpContext.Request.Path, RequestScheme = context.HttpContext.Request.Scheme, RequestLocalIp = (context.HttpContext.Request.HttpContext.Connection.LocalIpAddress.MapToIPv4().ToString() + ":" + context.HttpContext.Request.HttpContext.Connection.LocalPort), RequestRemoteIp = (context.HttpContext.Request.HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString() + ":" + context.HttpContext.Request.HttpContext.Connection.RemotePort) }; //获取请求的Body //数据流倒带 context.HttpContext.Request.EnableRewind(); if (context.HttpContext.Request.Body.CanSeek) { using (var requestSm = context.HttpContext.Request.Body) { requestSm.Position = 0; var reader = new StreamReader(requestSm, Encoding.UTF8); log.RequestBody = reader.ReadToEnd(); } } //将当前 http 响应Body 转换为 IReadableBody if (context.HttpContext.Response.Body is IReadableBody body) { if (body.IsRead) { log.ResponseBody = await body.ReadAsStringAsync(); } } if (string.IsNullOrEmpty(log.ResponseBody) == false && log.ResponseBody.Length > 200) { log.ResponseBody = log.ResponseBody.Substring(0, 200) + "......"; } LogHelper.Debug(log); } /// <summary> /// Action 执行前 /// </summary> /// <param name="context"></param> public void OnActionExecuting(ActionExecutingContext context) { //设置 Http请求响应内容设为可读 if (context.HttpContext.Response.Body is IReadableBody responseBody) { responseBody.IsRead = true; } } /// <summary> /// Action 执行后 /// </summary> /// <param name="context"></param> public void OnActionExecuted(ActionExecutedContext context) { } }
步骤6 对需要记录请求上下文日志的接口加上特性 [ApiFilter]
[ApiFilter] [Route("api/[controller]/[Action]")] [ApiController] public class DemoController : ControllerBase { ....... }
Demo 地址:https://github.com/intotf/netCore/tree/master/WebFilters