无论是进行ASP.NET Core MVC还是APS.NET Core Web API项目开发,我们都应该对请求数据的合法性进行校验。
比如重置密码操作中,我们需要检查两次输入的密码是否一致,以及密码长度是否符合要求。
对请求数据进行充分的合法性校验不仅有助于提升用户界面的友好性,而且有助于提高后台程序的安全性和稳定性。
为了提高响应速度和界面的可用性,一般在客户端我们都会对用户填写的数据进行校验,这样不需要把数据发送到服务器端,用户就会知道数据填写错误。
但是我们不能完全依赖客户端的校验。
不仅因为恶意用户可以绕过客户端校验直接向服务器发起请求,而且服务器端也需要对于客户端开发人员对数据校验不到位的地方做兜底工作。
因此,服务器端的数据校验必不可少。
1. .NET Core内置数据校验的不足
.NET Core中内置了对数据校验的支持,在System.ComponentModel.DataAnnotations命名空间下定义了非常多的校验规则Attribute。
比如[Required]用来设置值必须是非空的、[EmailAddress]用来设置值必须是Email格式的、[RegularExpression]用来根据给定的正则表达式对数据进行校验。
我们也可以使用CustomVaildationAttribute或者模型类实现IValidatableObject接口来编写自定义的校验规则。
关于.NET Core内部数据校验机制的用法可以参考官方文档。
.NET Core内置的校验机制有以下几个问题。
- 无论是通过在属性上标注校验规则Attribute的方式,还是实现了IValidatableObject接口的方式,我们的校验规则都是和模型类耦合在一起的,这违反了面向对象的”单一职责原则”
- .NET Core中内置的校验规则不够多,很多常用的校验需求都需要我们编写自定义校验规则。
2. FluentValidation的基本使用
FluentValidation可以让我们用类似于EF Core中Fluent API的方式进行校验规则的配置,也就是我们可以把对模型类的校验放到单独的校验类中。
FluentValidation可以用于控制台、WPF、ASP.NET Core等各种.NET项目中,这里只讲解它在ASP.NET Core项目中的用法。
第1步,在项目中安装NuGet包FluentValidation.AspNetCore
第2步,在Program.cs中添加注册相关服务的代码,如以下代码所示:
//获取入口项目的程序集
builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
第3步,编写一个模型类Login2Request,如以下代码:
public record Login2Request(string Emial,string Password,string Password2);
可以看到,这里的Login2Request类只是一个普通的C#类,没有标注任何的Attribute或者实现任何的接口,这个类的唯一职责就是传递数据。
第4步,编写一个继承自己AbstractValidator的数据校验类,如以下代码所示:
public class Login2RequestValidator:AbstractValidator<Login2Request>
{
public Login2RequestValidator()
{
RuleFor(x => x.Emial).NotNull().NotEmpty().EmailAddress()
.Must(b => b.EndsWith("@qq.com") || b.EndsWith("@163.com"))//Must编写自定义校验规则s
.WithMessage("只支持QQ和163邮箱");
RuleFor(x => x.Password).NotNull().NotEmpty().Length(3, 10)
.WithMessage("密码长度必须介于3到10之间")
.Equal(x => x.Password2).WithMessage("两次密码必须一致");
}
}
数据校验类一般继承自己AbstractValidator,AbstractValidator类是一个泛型类,我们需要通过泛型参数指定这个数据校验类对哪个类进行校验;
校验规则写到校验类的构造方法中;
FluentValidation内置的校验规则有默认的报错信息,我们也可以通过WithMessage方法自定义报错信息。
第5步,我们编写一个操作方法,用Login2Request作为参数,然后向操作方法发送非法的数据,响应的报文如图:
![](https://ichiblog.cn/wp-content/uploads/2024/10/图片-16-1024x334.png)
可以看到,使用FluentValidation以后,我们可以把数据校验的规则写到单独的数据校验类中。
这样模型类和数据校验类各司其职,符合”单一职责原则”,而且在FluentValidation中编写自定义校验代码也更加简单。
3. FluentValidation中注入服务
在编写数据校验代码的时候,有时候我们需要调用依赖注入容器中的服务,FluentValidation中的数据校验类是通过依赖注入容器实例化的。
因此我们同样可以通过构造方法来向数据校验类中注入服务。
假如数据库的一张表中记录着系统中已有的用户名、密码等信息,用户表的实体类为User。
我们通过DBContext来读取数据库。
下面来实现在登录的时候检查用户名是否存在的校验类。
定义一个类Login3Request,这个类包含UserName、Password两个属性。
然后我们再编写一个对Login3Request进行校验的类,如以下代码所示:
public class Login3RequestValidator() : AbstractValidator<Login3Request>
{
//通过构造方法注入了DbContext
public Login3RequestValidator(MySQLContext _context) : this()
{
RuleFor(x => x.UserName).NotNull()
.Must(name => _context.Set<Users>().Any(u => u.UserName == name))//使用Context检查用户名是否存在
.WithMessage(c => $"用户名{c.UserName}不存在");
}
}
由于异步代码通常能给系统带来更好的吞吐量,因此我们编写代码的原则是”能用异步代码就不要用同步代码”。
在FluentValidation中我们可以用MustAsync和AnyAsync来编写异步校验规则,如以下代码所示:
public class Login3RequestValidator() : AbstractValidator<Login3Request>
{
//通过构造方法注入了DbContext
public Login3RequestValidator(MySQLContext _context) : this()
{
RuleFor(x => x.UserName).NotNull()
.MustAsync((name,_) => _context.Set<Users>().AnyAsync(u => u.UserName == name, cancellationToken: _))
// 使用Context检查用户名是否存在
.WithMessage(c => $"用户名{c.UserName}不存在");
}
}