从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之六使用过滤器进行全局请求数据验证
在 上一篇 中讲到了在NetCore项目中如何进行全局异常处理,当手动抛出或系统未处理异常出现时进行的一个拦截处理。
本节中将讲到API请求模型的一个验证,先抛出几个问题,
- 为什么要使用模型验证?
对于我的了解来说,一般用户并不会都是输入的有效数据,这可能在应用程序中使用到这些数据时会产生一些意想不到的错误。 - 有什么作用?
使用模型验证是为了确保请求的数据在程序中能够有效使用,也是为了避免出现一些异常情况,还是就是可以不用在接口代码中再去关系模型数据的正确性,因为已经通过了模型验证。 - 如何使用?
MVC 对模型验证提供了较好的支持,提供了很多特性,可以通过 Model 元数据设置验证规则、用 ModelState 来处理错误信息、获取错误信息等。
在ASP.NET Core MVC 中提供了很多内置特性,一下是一些比较常用的内置特性:
以下是一些内置验证特性:
- [CreditCard] :验证属性是否有信用卡格式。
- [Compare] :验证模型中的两个属性是否匹配。
- [EmailAddress] :验证属性是否有电子邮件格式。
- [Phone] :验证属性是否有电话号码格式。貌似我们的号码无法使用这个,还是推荐使用正则特性
- [Range] :验证属性值是否在指定范围内。
- [RegularExpression] :验证属性值是否与指定的正则表达式匹配。
- [Required] :验证字段是否非 NULL。
- [StringLength] :验证字符串属性值是否未超过指定长度限制。
- [Url] :验证属性是否有 URL 格式。
- [Remote] :通过调用服务器上的操作方法,验证客户端上的输入。
除了这些内置特性之外,还可以添加自定义特性。为了方便示例,先创建一个 ClassicTestEqualAttribute 自定义属性,验证字段值是否等于内置值
/// <summary> /// 验证值是否等于内置值 /// </summary> public class ClassicTestEqualAttribute : ValidationAttribute { private string _bulitIn; private string _cusValid; public ClassicTestEqualAttribute(string bulitIn) { _bulitIn = bulitIn; } protected override ValidationResult IsValid( object value, ValidationContext validationContext) { var movie = (TestValidModel)validationContext.ObjectInstance; _cusValid = movie.CusValid; var cusValid = (string)value;//两种方式获取该字段值 - 也可以获取其它字段值 if (_bulitIn != cusValid) { return new ValidationResult(GetErrorMessage()); } return ValidationResult.Success; } public string CusValid => _cusValid; public string GetErrorMessage() { return $"测试模型中的{_bulitIn}不等于内置值"; } }
然后再创建一个测试model用来测试
public class TestValidModel { [Required(ErrorMessage = "请输入名字"), RegularExpression("^(?!_)(?!.*?_$)[a-zA-Z0-9_]{4,12}$", ErrorMessage = "登录名不符合规则,请输入4-12位不包含特殊字符的数据")] public string Name { get; set; } [Range(22,35,ErrorMessage = "年龄段应在22-35之间")] public int Age { get; set; } [ClassicTestEqual("test")] public string CusValid { get; set; } [StringLength(3,ErrorMessage = "字符串长度请控制在3个以内")] public string Role { get; set; } [Required(ErrorMessage = "请输入邮件")] [EmailAddress(ErrorMessage = "邮件格式不正确")] public string Email { get; set; } [Required(ErrorMessage = "请输入电话"), RegularExpression("^1[3|4|5|6|7|8|9][0-9]{9}$", ErrorMessage = "电话号码格式不正确")] public long Phone { get; set; } }
这个时候再添加一个测试方法,然后通过 Postman 来调用接口,调用时我们body什么都不传试一下。 PS:Postman是一个优秀的接口测试软件。
在这里我们能看到模型验证已经是失败的了,至于为什么错误个数只有5个,是因为在 Startup 类中进行了限制,使验证错误最大个数只有五个。
services.AddMvc 中更改 MaxModelValidationErrors 即可。
services.AddMvc(options => { options.MaxModelValidationErrors = 5;//验证错误最大个数 options.AllowValidatingTopLevelNodes = true;//是否允许验证顶级节点 接口方法参数 options.Filters.Add(new ExceptionFilter());//添加异常处理过滤器 }).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
在请求接口传入符合要求的数据时,模型验证就会通过。
在这里是可以从ModelState 中获取到错误信息的,只是为了演示模型验证的效果就没有写。不可能每次在需要做模型验证时都这样去写一遍这样的代码,因此,我们使用过滤器来进行全局的请求模型验证。
首先我们添加模型验证的方法,继承 ActionFilterAttribute 属性
/// <inheritdoc /> /// <summary> /// 模型数据验证 /// </summary> public class ModelValid : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { //判断模型是否验证通过 if (filterContext.ModelState.ErrorCount == 0 && filterContext.ModelState.IsValid) return; var errMsg = new StringBuilder(); foreach (var modelStateKey in filterContext.ModelState.Keys) { var value = filterContext.ModelState[modelStateKey]; foreach (var error in value.Errors) { if (!string.IsNullOrEmpty(error.ErrorMessage)) { errMsg.Append(error.ErrorMessage + ","); } } } if (errMsg.Length > 0) { errMsg.Remove(errMsg.Length - 1, 1); } if (filterContext.Controller is BaseController controller) filterContext.Result = controller.Fail(1005, $"请求数据验证失败,{errMsg}");//1005为自定义的错误Code else throw new CustomSystemException("默认Controller都要继承BaseController,以实现全局模型的验证、错误提醒",999);//999为自定义的错误Code } }
然后将该模型验证的过滤器属性添加到基类控制器 BaseController 上,因为每个控制器都会继承该控制器,而这个模型验证过滤器会在每次请求时触发。
这样添加完成后在每次接口访问时都会进行模型的验证。这时将先前model测试方法中的判断给注释掉,然后运行重新访问接口测试看下效果呢?
这里只有当模型完全验证通过才会去进行业务逻辑处理。否则是不会执行业务就会直接返回模型验证错误消息。PS:上面值得一说的是,为什么电话号码没有输,不会返回“请输入电话号码”。是因为 long 类型是没有NULL的,所以在没有输电话号码时,phone字段是默认值,即 Phone = default(long) // 0 只有把 long 型 换成 long? 或者 Nullable<long> 时才能在没有输入电话号码时触发 Required 特性。因为已经改变成可空类型了。
上面是在执行前进行模型的验证,我们还可以在执行一系列业务逻辑后再次验证模型中的数据是否满足要求,只要执行 TryValidateModel(validModel); 即可。
测试接口方法改成如下:
[HttpPost] [Route("testvalidmodel")] public ActionResult ValidModel([FromBody]TestValidModel validModel) { // if (!ModelState.IsValid) // { // return Fail(1005, "模型验证失败"); // } validModel.Age = 18; TryValidateModel(validModel); var b = ModelState.IsValid; return Succeed("测试"); }
然后再次用上面访问成功的模型,打断点调试可以得到验证结果为false,其实在这个地方可以将再次验证的方法添加到基类控制器中去,若验证失败,则直接返回验证失败结果。
除了进行模型验证之外,还能进行顶级节点的验证,也就是接口方法上的参数直接验证。但是一般不推荐这样使用。得先在 Startup 类中做一点更改,上面已经有代码贴出来了。示例代码如下:
[AcceptVerbs("Get", "Post")]//允许get 和post方法 [Route("validphone")] public ActionResult TestValid([Required(ErrorMessage = "请输入电话号码")] [RegularExpression(@"^1[3|4|5|6|7|8|9][0-9]{9}$", ErrorMessage = "不是一个有效的手机号")] string phone) { //phone = "123"; //TryValidateModel(phone);//单个字段该方法无效 应使用类 if (!ModelState.IsValid) { var errMsg = new StringBuilder(); foreach (var modelStateKey in ModelState.Keys) { var value = ModelState[modelStateKey]; foreach (var error in value.Errors) { errMsg.Append(error.ErrorMessage + ","); } } errMsg.Remove(errMsg.Length - 1, 1); return Fail(1, errMsg.ToString()); } return Succeed("测试"); }
执行效果就不贴图了,凡事只要在自己尝试过后才会深入了解。
在下一篇中将介绍如何在NetCore中通过jwt生成token,并进行token验证,刷新等个人想法。
有需要源码的可通过此 GitHub 链接拉取 觉得还可以的给个 start 哦,谢谢!
学习本是一个不断模仿、练习、创新、超越的过程。 由于博主能力有限,文中可能存在描述不正确,欢迎指正、补充! 感谢您的阅读,麻烦动动手指点个推荐哟。
-------------------------------------------------------------------------------------------------------------------------------------------------------------