参数校验的意义
在实际项目开发中,无论任何方式、任何规模的开发模式,项目中都离不开对接入数据模型参数的合法性校验,目前普片的开发模式基本是前后端分离,当用户在前端页面中输入一些表单数据时,点击提交按钮,触发请求目标服务器的一系列后续操作,在这中间的执行过程中(标准做法推荐)无论是前端代码部分,还是服务端代码部分都应该有针对用户输入数据的合法性校验,典型做法如下:
前端部分
:当用户在页面输入表单数据时,前端监听页面表单事件触发相应的数据合法性校验规则,当数据非法时,合理的提示用户数据错误,只有当所有表单数据都校验通过后,才继续提交数据给目标后端对应的接口;后端部分
:当前端数据合法校验通过后,向目标服务器提交表单数据时,服务端接收到相应的提交数据,在入口源头出就应该触发相关的合法性校验规则,当数据都校验通过后,继续执行后续的相关业务逻辑处理,反之则响应相关非法数据的提示信息;
特别说明:在实际的项目中,无论前端部分还是服务端部分,参数的校验都是很有必要性的。无效的参数,可能会导致应用程序的异常和一些不可预知的错误行为。
常用的参数校验项
这里例举一些项目中比较常用的参数模型校验项,如下所示:
Name:姓名校验,比如需要是纯汉字的姓名;
Password:密码强度验证,比如要求用户输入必须包含大小写字母、数字和特殊符号的强密码;
QQ:QQ 号码验证,是否是有效合法的 QQ 号码;
China Postal Code:中国邮政编码;
IP Address:IPV4 或者 IPV6 地址验证;
Phone:手机号码或者座机号码合法性验证;
ID Card:身份证号码验证,比如:15 位和 18 位数身份证号码;
Email Address:邮箱地址的合法性校验;
String:字符串验证,比如字段是否不为 null、长度是否超限;
URL:验证属性是否具有 URL 格式;
Number:数值型参数校验,数值范围校验,比如非负数,非负整数,正整数等;
File:文件路径及扩展名校验;
对于参数校验,常见的方式有正则匹配校验,通过对目标参数编写合法的正则表达式,实现对参数合法性的校验。
.NET 中内置 DataAnnotations 提供的特性校验
上面我们介绍了一些常用的参数验证项,接下来我们来了解下在 .NET
中内置提供的 DataAnnotations
数据注解,该类提供了一些常用的验证参数特性。
官方解释:
提供用于为
ASP.NET MVC
和ASP.NET
数据控件定义元数据的特性类。该类位于
System.ComponentModel.DataAnnotations
命名空间。
关于 DataAnnotations 中的特性介绍
让我们可以通过这些特性对 API
请求中的参数进行验证,常用的特性一般有:
**[ValidateNever]**:指示应从验证中排除属性或参数。
**[CreditCard]**:验证属性是否具有信用卡格式。
**[Compare]**:验证模型中的两个属性是否匹配。
**[EmailAddress]**:验证属性是否具有电子邮件格式。
**[Phone]**:验证属性是否具有电话号码格式。
**[Range]**:验证属性值是否位于指定范围内。
**[RegularExpression]**:验证属性值是否与指定的正则表达式匹配。
**[Required]**:验证字段是否不为 null。
**[StringLength]**:验证字符串属性值是否不超过指定的长度限制。
**[Url]**:验证属性是否具有 URL 格式。
其中 RegularExpression
特性,基于正则表达式可以扩展实现很多常用的验证类型,下面的( 基于 DataAnnotations 的通用模型校验封装
)环节举例说明。
关于该类更多详细信息请查看,
https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.dataannotations?view=net-7.0
基于 DataAnnotations 的通用模型校验封装
此处主要是使用了 Validator.TryValidateObject()
方法:
Validator.TryValidateObject(object instance, ValidationContext validationContext, ICollection<ValidationResult>? validationResults, bool validateAllProperties);
Validator
类提供如下校验方法:
基于 DataAnnotations 的特性校验助手实现步骤
错误成员对象类
ErrorMember
namespace Jeff.Common.Validatetion;/// <summary>
/// 错误成员对象
/// </summary>
public class ErrorMember
{/// <summary>/// 错误信息/// </summary>public string? ErrorMessage { get; set; }/// <summary>/// 错误成员名称/// </summary>public string? ErrorMemberName { get; set; }
}
验证结果类
ValidResult
namespace Jeff.Common.Validatetion;/// <summary>
/// 验证结果类
/// </summary>
public class ValidResult
{public ValidResult(){ErrorMembers = new List<ErrorMember>();}/// <summary>/// 错误成员列表/// </summary>public List<ErrorMember> ErrorMembers { get; set; }/// <summary>/// 验证结果/// </summary>public bool IsVaild { get; set; }
}
定义操作正则表达式的公共类
RegexHelper
(基于RegularExpression
特性扩展)
using System;
using System.Net;
using System.Text.RegularExpressions;namespace Jeff.Common.Validatetion;/// <summary>
/// 操作正则表达式的公共类
/// Regex 用法参考:https://learn.microsoft.com/zh-cn/dotnet/api/system.text.regularexpressions.regex.-ctor?redirectedfrom=MSDN&view=net-7.0
/// </summary>
public class RegexHelper
{#region 常用正则验证模式字符串public enum ValidateType{Email, // 邮箱TelePhoneNumber, // 固定电话(座机)MobilePhoneNumber, // 移动电话Age, // 年龄(1-120 之间有效)Birthday, // 出生日期Timespan, // 时间戳IdentityCardNumber, // 身份证IpV4, // IPv4 地址IpV6, // IPV6 地址Domain, // 域名English, // 英文字母Chinese, // 汉字MacAddress, // MAC 地址Url, // URL }private static readonly Dictionary<ValidateType, string> keyValuePairs = new Dictionary<ValidateType, string>{{ ValidateType.Email, _Email },{ ValidateType.TelePhoneNumber,_TelephoneNumber }, { ValidateType.MobilePhoneNumber,_MobilePhoneNumber }, { ValidateType.Age,_Age }, { ValidateType.Birthday,_Birthday }, { ValidateType.Timespan,_Timespan }, { ValidateType.IdentityCardNumber,_IdentityCardNumber }, { ValidateType.IpV4,_IpV4 }, { ValidateType.IpV6,_IpV6 }, { ValidateType.Domain,_Domain }, { ValidateType.English,_English }, { ValidateType.Chinese,_Chinese }, { ValidateType.MacAddress,_MacAddress }, { ValidateType.Url,_Url }, };public const string _Email = @"^(\w)+(\.\w)*@(\w)+((\.\w+)+)$"; // ^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$ , [A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}public const string _TelephoneNumber = @"(d+-)?(d{4}-?d{7}|d{3}-?d{8}|^d{7,8})(-d+)?"; //座机号码(中国大陆)public const string _MobilePhoneNumber = @"^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$"; //移动电话public const string _Age = @"^(?:[1-9][0-9]?|1[01][0-9]|120)$"; // 年龄 1-120 之间有效public const string _Birthday = @"^((?:19[2-9]\d{1})|(?:20(?:(?:0[0-9])|(?:1[0-8]))))((?:0?[1-9])|(?:1[0-2]))((?:0?[1-9])|(?:[1-2][0-9])|30|31)$";public const string _Timespan = @"^15|16|17\d{8,11}$"; // 目前时间戳是15开头,以后16、17等开头,长度 10 位是秒级时间戳的正则,13 位时间戳是到毫秒级的。public const string _IdentityCardNumber = @"^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$|^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$";public const string _IpV4 = @"^((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}$";public const string _IpV6 = @"^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$";public const string _Domain = @"^[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?$";public const string _English = @"^[A-Za-z]+$";public const string _Chinese = @"^[\u4e00-\u9fa5]{0,}$";public const string _MacAddress = @"^([0-9A-F]{2})(-[0-9A-F]{2}){5}$";public const string _Url = @"^[a-zA-z]+://(\w+(-\w+)*)(\.(\w+(-\w+)*))*(\?\S*)?$";#endregion/// <summary>/// 获取验证模式字符串/// </summary>/// <param name="validateType"></param>/// <returns></returns>public static (bool hasPattern, string pattern) GetValidatePattern(ValidateType validateType) {bool hasPattern = keyValuePairs.TryGetValue(validateType, out string? pattern);return (hasPattern, pattern ?? string.Empty);}#region 验证输入字符串是否与模式字符串匹配/// <summary>/// 验证输入字符串是否与模式字符串匹配/// </summary>/// <param name="input">输入的字符串</param>/// <param name="validateType">模式字符串类型</param>/// <param name="matchTimeout">超时间隔</param>/// <param name="options">筛选条件</param>/// <returns></returns>public static (bool isMatch, string info) IsMatch(string input, ValidateType validateType, TimeSpan matchTimeout, RegexOptions options = RegexOptions.None){var (hasPattern, pattern) = GetValidatePattern(validateType);if (hasPattern && !string.IsNullOrWhiteSpace(pattern)){bool isMatch = IsMatch(input, pattern, matchTimeout, options);if (isMatch) return (true, "Format validation passed."); // 格式验证通过。else return (false, "Format validation failed."); // 格式验证未通过。}return (false, "Unknown ValidatePattern."); // 未知验证模式}/// <summary>/// 验证输入字符串是否与模式字符串匹配,匹配返回true/// </summary>/// <param name="input">输入字符串</param>/// <param name="pattern">模式字符串</param> /// <returns></returns>public static bool IsMatch(string input, string pattern){return IsMatch(input, pattern, TimeSpan.Zero, RegexOptions.IgnoreCase);}/// <summary>/// 验证输入字符串是否与模式字符串匹配,匹配返回true/// </summary>/// <param name="input">输入的字符串</param>/// <param name="pattern">模式字符串</param>/// <param name="matchTimeout">超时间隔</param>/// <param name="options">筛选条件</param>/// <returns></returns>public static bool IsMatch(string input, string pattern, TimeSpan matchTimeout, RegexOptions options = RegexOptions.None){return Regex.IsMatch(input, pattern, options, matchTimeout);}#endregion
}
定义验证结果统一模型格式类
ResponseInfo
(此类通常也是通用的数据响应模型类)
namespace Jeff.Common.Model;public sealed class ResponseInfo<T> where T : class
{/*Microsoft.AspNetCore.Http.StatusCodesSystem.Net.HttpStatusCode*//// <summary>/// 响应代码(自定义)/// </summary>public int Code { get; set; }/// <summary>/// 接口状态/// </summary>public bool Success { get; set; }#region 此处可以考虑多语言国际化设计(语言提示代号对照表)/// <summary>/// 语言对照码,参考:https://blog.csdn.net/shenenhua/article/details/79150053/// </summary>public string Lang { get; set; } = "zh-cn";/// <summary>/// 提示信息/// </summary>public string Message { get; set; } = string.Empty;#endregion/// <summary>/// 数据体/// </summary>public T? Data { get; set; }
}
实现验证助手类
ValidatetionHelper
,配合System.ComponentModel.DataAnnotations
类使用
// 数据注解,https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.dataannotations?view=net-7.0
using System.ComponentModel.DataAnnotations;
using Jeff.Common.Model;namespace Jeff.Common.Validatetion;/// <summary>
/// 验证助手类
/// </summary>
public sealed class ValidatetionHelper
{/// <summary>/// DTO 模型校验/// </summary>/// <param name="value"></param>/// <returns></returns>public static ValidResult IsValid(object value){var result = new ValidResult();try{var validationContext = new ValidationContext(value);var results = new List<ValidationResult>();bool isValid = Validator.TryValidateObject(value, validationContext, results, true);result.IsVaild = isValid;if (!isValid){foreach (ValidationResult? item in results){result.ErrorMembers.Add(new ErrorMember(){ErrorMessage = item.ErrorMessage,ErrorMemberName = item.MemberNames.FirstOrDefault()});}}}catch (ValidationException ex){result.IsVaild = false;result.ErrorMembers = new List<ErrorMember>{new ErrorMember(){ErrorMessage = ex.Message,ErrorMemberName = "Internal error"}};}return result;}/// <summary>/// DTO 模型校验统一响应信息/// </summary>/// <typeparam name="T"></typeparam>/// <param name="model"></param>/// <returns></returns>public static ResponseInfo<ValidResult> GetValidInfo<T>(T model) where T : class{var result = new ResponseInfo<ValidResult>();var validResult = IsValid(model);if (!validResult.IsVaild){result.Code = 420;result.Message = "DTO 模型参数值异常";result.Success = false;result.Data = validResult;}else{result.Code = 200;result.Success = true;result.Message = "DTO 模型参数值合法";}return result;}
}
如何使用 DataAnnotations 封装的特性校验助手?
首先定义一个数据模型类(
DTO
),添加校验特性ValidationAttribute
using System.ComponentModel.DataAnnotations;
using Jeff.Common.Validatetion;namespace Jeff.Comm.Test;public class Person
{[Display(Name = "姓名"), Required(ErrorMessage = "{0}必须填写")]public string Name { get; set; }[Display(Name = "邮箱")][Required(ErrorMessage = "{0}必须填写")][RegularExpression(RegexHelper._Email, ErrorMessage = "RegularExpression: {0}格式非法")][EmailAddress(ErrorMessage = "EmailAddress: {0}格式非法")]public string Email { get; set; }[Display(Name = "Age年龄")][Required(ErrorMessage = "{0}必须填写")][Range(1, 120, ErrorMessage = "超出范围")][RegularExpression(RegexHelper._Age, ErrorMessage = "{0}超出合理范围")]public int Age { get; set; }[Display(Name = "Birthday出生日期")][Required(ErrorMessage = "{0}必须填写")][RegularExpression(RegexHelper._Timespan, ErrorMessage = "{0}超出合理范围")]public TimeSpan Birthday { get; set; }[Display(Name = "Address住址")][Required(ErrorMessage = "{0}必须填写")][StringLength(200, MinimumLength = 10, ErrorMessage = "{0}输入长度不正确")]public string Address { get; set; }[Display(Name = "Mobile手机号码")][Required(ErrorMessage = "{0}必须填写")][RegularExpression(RegexHelper._MobilePhoneNumber, ErrorMessage = "{0}格式非法")]public string Mobile { get; set; }[Display(Name = "Salary薪水")][Required(ErrorMessage = "{0}必须填写")][Range(typeof(decimal), "1000.00", "3000.99")]public decimal Salary { get; set; }[Display(Name = "MyUrl连接")][Required(ErrorMessage = "{0}必须填写")][Url(ErrorMessage = "Url:{0}格式非法")][RegularExpression(RegexHelper._Url, ErrorMessage = "RegularExpression:{0}格式非法")]public string MyUrl { get; set; }
}
控制台调用通用校验助手验证方法
ValidatetionHelper.IsValid()
或ValidatetionHelper.GetValidInfo()
// 通用模型数据验证测试
static void ValidatetionTest()
{var p = new Person{Name = "",Age = -10,Email = "www.baidu.com",MobilePhoneNumber = "12345",Salary = 4000,MyUrl = "aaa"};// 调用通用模型校验var result = ValidatetionHelper.IsValid(p);if (!result.IsVaild){foreach (ErrorMember errorMember in result.ErrorMembers){// 控制台打印字段验证信息Console.WriteLine($"{errorMember.ErrorMemberName}:{errorMember.ErrorMessage}");}}Console.WriteLine();// 调用通用模型校验,返回统一数据格式var validInfo = ValidatetionHelper.GetValidInfo(p);var options = new JsonSerializerOptions{Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // 设置中文编码乱码WriteIndented = false};string jsonStr = JsonSerializer.Serialize(validInfo, options);Console.WriteLine($"校验结果返回统一数据格式:{jsonStr}");
}
在控制台Program.Main
方法中调用 ValidatetionTest()
方法:
internal class Program
{static void Main(string[] args){Console.WriteLine("Hello, DataAnnotations!");{#region 数据注解(DataAnnotations)模型验证ValidatetionTest(); #endregion}Console.ReadKey();}
启动控制台,输出如下信息:
如何实现自定义的验证特性?
当我们碰到这些参数需要验证的时候,而上面内置类提供的特性又不能满足需求时,此时我们可以实现自定义的验证特性来满足校验需求,按照微软给出的编码规则,我们只需继承 ValidationAttribute
类,并重写 IsValid()
方法即可。
自定义校验特性案例
比如实现一个密码强度的验证,实现步骤如下:
定义密码强度规则,只包含英文字母、数字和特殊字符的组合,并且组合长度至少 8 位数;
/// <summary>
/// 只包含英文字母、数字和特殊字符的组合
/// </summary>
/// <returns></returns>
public static bool IsCombinationOfEnglishNumberSymbol(string input, int? minLength = null, int? maxLength = null)
{var pattern = @"(?=.*\d)(?=.*[a-zA-Z])(?=.*[^a-zA-Z\d]).";if (minLength is null && maxLength is null)pattern = $@"^{pattern}+$";else if (minLength is not null && maxLength is null)pattern = $@"^{pattern}{{{minLength},}}$";else if (minLength is null && maxLength is not null)pattern = $@"^{pattern}{{1,{maxLength}}}$";elsepattern = $@"^{pattern}{{{minLength},{maxLength}}}$";return Regex.IsMatch(input, pattern);
}
实现自定义特性
EnglishNumberSymbolCombinationAttribute
,继承自ValidationAttribute
;
using System.ComponentModel.DataAnnotations;namespace Jeff.Common.Validatetion.CustomAttributes;/// <summary>
/// 是否是英文字母、数字和特殊字符的组合
/// </summary>
public class EnglishNumberSymbolCombinationAttribute : ValidationAttribute
{/// <summary>/// 默认的错误提示信息/// </summary>private const string error = "无效的英文字母、数字和特殊字符的组合";protected override ValidationResult IsValid(object value, ValidationContext validationContext){if (value is null) return new ValidationResult("参数值为 null");//if (value is null)//{// throw new ArgumentNullException(nameof(attribute));//}// 验证参数逻辑 value 是需要验证的值,而 validationContext 中包含了验证相关的上下文信息,这里可自己封装一个验证格式的 FormatValidation 类if (FormatValidation.IsCombinationOfEnglishNumberSymbol(value as string, 8))//验证成功返回 successreturn ValidationResult.Success;//不成功 提示验证错误的信息else return new ValidationResult(ErrorMessage ?? error);}
}
以上就实现了一个自定义规则的 自定义验证特性
,使用方式很简单,可以把它附属在我们 请求的参数
上或者 DTO 里的属性
,也可以是 Action 上的形参
,如下所示:
public class CreateDTO
{[Required]public string StoreName { get; init; }[Required]// 附属在 DTO 里的属性[EnglishNumberSymbolCombination(ErrorMessage = "UserId 必须是英文字母、数字和特殊符号的组合")]public string UserId { get; init; }
}
...
// 附属在 Action 上的形参
[HttpGet]
public async ValueTask<ActionResult> Delete([EnglishNumberSymbolCombination]string userId, string storeName)
该自定义验证特性还可以结合 DataAnnotations
内置的 [Compare]
特性,可以实现账号注册的密码确认验证(输入密码和确认密码是否一致性
)。关于更多自定义参数校验特性,感兴趣的小伙伴可参照上面案例的实现思路,自行扩展实现哟。
总结
对于模型参数的校验,在实际项目系统中是非常有必要性的(通常在数据源头提供验证),利用 .NET
内置的 DataAnnotations
(数据注解)提供的特性校验,可以很方便的实现通用的模型校验助手,关于其他特性的用法,请自行参考微软官方文档,这里注意下RegularExpressionAttribute
(指定 ASP.NET
动态数据中的数据字段值必须与指定的正则表达式匹配),该特性可以方便的接入正则匹配验证,当遇到复杂的参数校验时,可以快速方便的扩展自定义校验特性,从此告别传统编码中各种 if(xxx != yyyy)
判断的验证,让整体代码编写更佳简练干净。