從零開始搭建前後端分離的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驗證,重新整理等個人想法。
有需要原始碼的在下方評論或私信~給我的SVN訪客賬戶密碼下載,程式碼未放在GitHub上。
&n