.NET之过滤器常见操作1、异常处理
目的
通过异常过滤器实现业务异常捕捉.
操作
引用辅助包
<PackageReference Include="AzrngCommon" Version="1.2.6" />
主要使用该包内的返回类
过滤器
/// <summary>
/// 自定义全局异常过滤器
/// </summary>
public class CustomExceptionFilter : IExceptionFilter
{
private readonly IWebHostEnvironment _hostEnvironment;
private readonly ILogger<CustomExceptionFilter> _logger;
public CustomExceptionFilter(ILogger<CustomExceptionFilter> logger,
IWebHostEnvironment hostEnvironment)
{
_logger = logger;
_hostEnvironment = hostEnvironment;
}
public void OnException(ExceptionContext context)
{
//如果异常没有处理
if (context.ExceptionHandled)
return;
var result = new ResultModel
{
Code = "500",
IsSuccess = false,
Message = "系统异常,请联系管理员",
};
_logger.LogError($"异常 path:{context.Result} message:{context.Exception.Message} StackTrace:{context.Exception.StackTrace}");
if (_hostEnvironment.IsDevelopment())
{
result.Message += "," + context.Exception.Message;
}
context.Result = new JsonResult(result);
//异常已处理
context.ExceptionHandled = true;
}
}
全局使用
builder.Services.AddControllers(option =>
{
//添加全局过滤器
option.Filters.Add(typeof(CustomExceptionFilter));
});
总结
不能拦截处理Action以外的错误。
.NET之过滤器常见操作2、返回类处理
目的
通过返回过滤器实现返回类的处理,在最外层包一层公共返回类。
操作
准备
里面的ResultModel使用的是nuget的东西
<PackageReference Include="AzrngCommon" Version="1.2.4" />
返回过滤器处理
/// <summary>
/// 方案一:返回类处理(让返回结果外面包一层公共业务返回类)
/// </summary>
[AttributeUsage(AttributeTargets.All)]
public class CustomResultPackFilter : Attribute, IResultFilter
{
public void OnResultExecuted(ResultExecutedContext context)
{
}
public void OnResultExecuting(ResultExecutingContext context)
{
if (context.Result is EmptyResult)
{
context.Result = new OkObjectResult(new ResultModel
{
Code = "200",
IsSuccess = true,
Message = "成功"
});
return;
}
context.Result = new OkObjectResult(new ResultModel<object>
{
Code = "200",
IsSuccess = true,
Data = ((ObjectResult)context.Result).Value
});
}
}
/// <summary>
/// 方案二:返回类处理(让返回结果外面包一层公共业务返回类增加返回code和消息)
/// </summary>
[AttributeUsage(AttributeTargets.All)]
public class CustomResultPack2Filter : ActionFilterAttribute
{
public class ReturnDataFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext context)
{
if (context.Result is EmptyResult)
{
context.Result = new OkObjectResult(new ResultModel
{
Code = "200",
IsSuccess = true,
Message = "成功"
});
return;
}
context.Result = new OkObjectResult(new ResultModel<object>
{
Code = "200",
IsSuccess = true,
Data = ((ObjectResult)context.Result).Value
});
}
}
}
注册全局过滤器
services.AddControllers(options =>
{
option.Filters.Add(typeof(CustomResultPackFilter));
});
.NET之过滤器常见操作3、入参校验
目的
通过Action过滤器实现对一些常见的请求入参的校验。比如我们的接口中经常有患者ID字段,我们可以全局对该字段进行限制。
操作
编写过滤器
/// <summary>
/// 对模型验证过滤器
/// </summary>
public class ModelValidationFilter : ActionFilterAttribute
{
//实现目的:比如接口中的常用参数有患者ID,我们可以写过滤器统一校验患者ID是否有效
private readonly ILogger<ModelValidationFilter> _logger;
public ModelValidationFilter(ILogger<ModelValidationFilter> logger)
{
_logger = logger;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
if (context.HttpContext.Request.Query.TryGetValue("patientId", out StringValues patientIdValue))
{
if (int.TryParse(patientIdValue.FirstOrDefault(), out int patientId))
{
if (patientId == 0)
{
_logger.LogWarning($"{context.HttpContext.Request.Path} 患者标识无效");
context.Result = new BadRequestObjectResult("患者标识无效");
}
}
}
if (context.HttpContext.Request.Method == "POST" || context.HttpContext.Request.Method == "PUT")
{
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);//读取到Body后,重新设置Stream到起始位置
var stream = new StreamReader(context.HttpContext.Request.Body);
string body = stream.ReadToEndAsync().GetAwaiter().GetResult();
JObject jobject = JObject.Parse(body);
if (int.TryParse(jobject["patientId"].ToString(), out int patientId))
{
if (patientId == 0)
{
_logger.LogWarning($"{context.HttpContext.Request.Path} 患者标识无效");
context.Result = new BadRequestObjectResult("患者标识无效");
}
}
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);//读取到Body后,重新设置Stream到起始位置
}
}
}
全局使用
builder.Services.AddControllers(option =>
{
//添加全局过滤器
option.Filters.Add(typeof(ModelValidationFilter));
});
因为设计到读取请求体的操作,还需要借助中间件来设置可以重复读取流
//读取请求体设置可以重复读取
app.Use((context, next) =>
{
context.Request.EnableBuffering();
return next(context);
});
总结
可以实现URL、请求体中参数校验。
.NET之过滤器常见操作4、日志记录
目的
通过Action过滤器实现对请求日志的记录。
操作
编写过滤器
/// <summary>
/// 日志记录
/// </summary>
public class RequestParamRecordFilter : ActionFilterAttribute
{
//目的:记录请求的消息
private readonly ILogger<ModelValidationFilter> _logger;
public RequestParamRecordFilter(ILogger<ModelValidationFilter> logger)
{
_logger = logger;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
//设置可以多次读取
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);//读取到Body后,重新设置Stream到起始位置
var sr = new StreamReader(context.HttpContext.Request.Body);
var data = sr.ReadToEndAsync().GetAwaiter().GetResult();
_logger.LogInformation(
$"Time:{DateTime.Now:yyyy-MM-dd HH:mm:ss} requestUrl:{context.HttpContext.Request.Path} Method:{context.HttpContext.Request.Method} requestBodyData: " +
data);
//读取到Body后,重新设置Stream到起始位置
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);
_logger.LogInformation($"Host: {context.HttpContext.Request.Host.Host}");
_logger.LogInformation($"Client IP: {context.HttpContext.Connection.RemoteIpAddress}");
}
}
全局使用
builder.Services.AddControllers(option =>
{
//添加全局过滤器
option.Filters.Add(typeof(RequestParamRecordFilter));
});
因为涉及到读取请求体的操作,还需要借助中间件来设置可以重复读取流
//读取请求体设置可以重复读取
app.Use((context, next) =>
{
context.Request.EnableBuffering();
return next(context);
});
输出结果:
info: NetCoreFilterSample.CustomFilter.ModelValidationFilter[0]
Time:2022-02-19 00:07:04 requestUrl:/api/WeatherForecast/AddPatientEat Method:POST requestBodyData: {
"patientId": 10,
"eat": "string222"
}
info: NetCoreFilterSample.CustomFilter.ModelValidationFilter[0]
Host: localhost
info: NetCoreFilterSample.CustomFilter.ModelValidationFilter[0]
Client IP: ::1
总结
可以实现请求地址入参等参数记录。
.NET之过滤器常见操作5、幂等性
目的
通过请求地址作为key,搭配内存缓存,实现幂等性校验。
操作
因为本文使用到了IMemoryCache,所以还需要注入该服务
builder.Services.AddMemoryCache();
编写过滤器
/// <summary>
/// 接口幂等性处理
/// </summary>
public class IdempotentAttributeFilter : Attribute, IActionFilter
{
private readonly IMemoryCache _cache;
public IdempotentAttributeFilter(IMemoryCache cache)
{
_cache = cache;
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
//可以根据用户ID或者请求地址标识当前用户
var path = context.HttpContext.Request.Path;
var userId = "123456";//这个可以从上下文中获取
var key = "IdempotencyKey" + userId + path.ToString();
var method = context.HttpContext.Request.Method;
if (method == "POST" || method == "put")
{
//直接限制了该接口不允许一个用户在2秒内请求多次
var cacheData = _cache.Get<string>(key);
if (cacheData != null)
{
throw new ParameterException("不允许重复提交");
}
_cache.Set(key, "1", TimeSpan.FromSeconds(2));
}
}
}
更合适的写法是,使用redis(可以不怕服务部署多个节点),然后根据用户标识作为key,并且也要检验当前请求体的内容是不是也上一次也一样。
全局使用
builder.Services.AddControllers(option =>
{
//添加全局过滤器
option.Filters.Add(typeof(IdempotentAttributeFilter));
});
总结
将不带幂等性的接口(Post、Put),增加限制一个用户在2秒内只能请求1次,防止重复提交。
.NET之过滤器常见操作6、基于IP请求限制
目的
限制每一个ip客户端在指定的时间范围内请求的数量,防止恶意攻击。
操作
增加请求限制过滤器
/// <summary>
/// 根据ip接口请求限制
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class RequestLimitFilter : ActionFilterAttribute
{
private string Name { get; }
private int LimitRequestNum { get; set; }
private int Seconds { get; set; }
private MemoryCache _cache { get; } = new MemoryCache(new MemoryCacheOptions());
/// <summary>
/// 请求限制属性
/// </summary>
/// <param name="name">key</param>
/// <param name="limitRequestNum">限制的次数</param>
/// <param name="seconds">限制时间</param>
public RequestLimitFilter(string name, int limitRequestNum = 5, int seconds = 10)
{
Name = name;
LimitRequestNum = limitRequestNum;
Seconds = seconds;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
var ipAddress = context.HttpContext.Request.HttpContext.Connection.RemoteIpAddress;
var key = $"{Name}-{ipAddress}";
var prevReqCount = _cache.Get<int>(key);
if (prevReqCount >= LimitRequestNum)
{
context.Result = new ContentResult
{
Content = $"Request limit is exceeded. Try again in {Seconds} seconds.",
};
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.TooManyRequests;
}
else
{
_cache.Set(key, (prevReqCount + 1), TimeSpan.FromSeconds(Seconds));
}
}
}
使用的时候只需要在接口头部增加
[HttpGet]
[RequestLimit("DataGet", 5, 30)]
public IEnumerable<WeatherForecast> Get()
{
...
}
总结
通过借助内存缓存来存储,实现请求限制。
最后总结
上面只是列举了一部分使用场景,有些场景或许使用中间件处理更加合适,这个需要自行判断。还可以做匿名化处理等。