点击上方蓝字关注我们

0.简介

在平时开发 API 接口的时候需要对前端传入的参数进行校验之后才能进入业务逻辑进行处理,否则一旦前端传入一些非法/无效数据到 API 当中,轻则导致程序报错,重则导致整个业务流程出现问题。

用过传统 ASP.NET MVC 数据注解的同学应该知道,我们可以通过在 Model 上面指定各种数据特性,然后在前端调用 API 的时候就会根据这些注解来校验 Model 内部的字段是否合法。

1.启动流程

Abp 针对于数据校验分为两个地方进行,第一个是 MVC 的过滤器,也是我们最常使用的。第二个则是借助于 Castle 的拦截器实现的 DTO 数据校验功能,前者只能用于控制器方法,而后者则支持普通方法。

1.1 过滤器注入

在注入 Abp 的时候,通过 AddAbp() 方法内部的 ConfigureAspNetCore() 配置了诸多过滤器。

private static void ConfigureAspNetCore(IServiceCollection services, IIocResolver iocResolver)
{// ... 其他代码//Configure MVCservices.Configure<MvcOptions>(mvcOptions =>{mvcOptions.AddAbp(services);});// ... 其他代码
}

过滤器注入方法:

internal static class AbpMvcOptionsExtensions
{public static void AddAbp(this MvcOptions options, IServiceCollection services){// ... 其他代码AddFilters(options);// ... 其他代码}// ... 其他代码private static void AddFilters(MvcOptions options){// ... 其他过滤器注入// 注入参数验证过滤器options.Filters.AddService(typeof(AbpValidationActionFilter));// ... 其他过滤器注入}// ... 其他代码
}

1.2 拦截器注入

Abp 针对于验证拦截器的注册始于 AbpBootstrapper 类,该基类在之前曾经多次出现过,也就是在用户调用 IServiceCollection.AddAbp<TStartupModule>() 方法的时候会初始化该类的一个实例对象。在该类的构造函数当中,会调用一个 AddInterceptorRegistrars() 方法用于添加各种拦截器的注册类实例。代码如下:

public class AbpBootstrapper : IDisposable
{private AbpBootstrapper([NotNull] Type startupModule, [CanBeNull] Action<AbpBootstrapperOptions> optionsAction = null){// ... 其他代码if (!options.DisableAllInterceptors){AddInterceptorRegistrars();}}// ... 其他代码// 添加各种拦截器private void AddInterceptorRegistrars(){ValidationInterceptorRegistrar.Initialize(IocManager);AuditingInterceptorRegistrar.Initialize(IocManager);EntityHistoryInterceptorRegistrar.Initialize(IocManager);UnitOfWorkRegistrar.Initialize(IocManager);AuthorizationInterceptorRegistrar.Initialize(IocManager);}// ... 其他代码\
}

来到 ValidationInterceptorRegistrar 类型定义当中可以看到,其内部就是通过 Castle 的 IocContainer 来针对每次注入的应用服务应用上参数验证拦截器。

internal static class ValidationInterceptorRegistrar
{public static void Initialize(IIocManager iocManager){iocManager.IocContainer.Kernel.ComponentRegistered += Kernel_ComponentRegistered;}private static void Kernel_ComponentRegistered(string key, IHandler handler){// 判断是否实现了 IApplicationService 接口,如果实现了,则为该对象添加拦截器if (typeof(IApplicationService).GetTypeInfo().IsAssignableFrom(handler.ComponentModel.Implementation)){handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(ValidationInterceptor)));}}
}

2.代码分析

从 Abp 库代码当中我们可以知道其拦截器与过滤器是在何时被注入的,下面我们就来具体分析一下他们的处理逻辑。

2.1 过滤器代码分析

Abp 在框架初始化的时候就将 AbpValidationActionFilter 添加到 MVC 的配置当中,其自定义实现的拦截器实现了 IAsyncActionFilter 接口,也就是说当每次接口被调用的时候都会进入该拦截器的内部。

public class AbpValidationActionFilter : IAsyncActionFilter, ITransientDependency
{// Ioc 解析器,用于解析各种注入的组件private readonly IIocResolver _iocResolver;// Abp 针对与 ASP.NET Core 的配置项,主要作用是判断用户是否需要检测控制器方法private readonly IAbpAspNetCoreConfiguration _configuration;public AbpValidationActionFilter(IIocResolver iocResolver, IAbpAspNetCoreConfiguration configuration){_iocResolver = iocResolver;_configuration = configuration;}public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next){// ... 处理逻辑}
}

在内部首先是结合配置项判断用户是否禁用了 MVC Controller 的参数验证功能,禁用了则不进行任何操作。

public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{// 判断是否禁用了控制器检测if (!_configuration.IsValidationEnabledForControllers || !context.ActionDescriptor.IsControllerAction()){await next();return;}// 针对应用服务增加一个验证完成标识using (AbpCrossCuttingConcerns.Applying(context.Controller, AbpCrossCuttingConcerns.Validation)){// 解析出方法验证器,传入请求上下文,并且调用这些验证器具体的验证方法using (var validator = _iocResolver.ResolveAsDisposable<MvcActionInvocationValidator>()){validator.Object.Initialize(context);validator.Object.Validate();}await next();}
}

其实我们这里看到有一个 AbpCrossCuttingConcerns.Applying() 方法,那么该方法的作用是什么呢?

在这里我先大体讲述一下该方法的作用,该方法主要是向应用服务对象 (也就是继承了 ApplicationService 类的对象) 内部的 AppliedCrossCuttingConcerns 属性增加一个常量值,在这里也就是 AbpCrossCuttingConcerns.Validation 的值,也就是一个字符串。

那么其作用是什么呢,就是防止重复验证。从启动流程一节我们就已经知道 Abp 框架在启动的时候除了注入过滤器之外,还会注入拦截器进行接口参数验证,当过滤器验证过之后,其实没必要再使用拦截器进行二次验证。

所以在拦截器的 Intercept() 方法内部会有这样一句代码:

public void Intercept(IInvocation invocation)
{// 判断是否拥有处理过的标识if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Validation)){invocation.Proceed();return;}// ... 其他代码
}

解释完 AbpCrossCuttingConcerns.Applying() 之后,我们继续往下看代码。

// 解析出方法验证器,传入请求上下文,并且调用这些验证器具体的验证方法
using (var validator = _iocResolver.ResolveAsDisposable<MvcActionInvocationValidator>())
{validator.Object.Initialize(context);validator.Object.Validate();
}await next();

这里就比较简单了,过滤器通过 IocResolver 解析出来了一个 MvcActionInvocationValidator 对象,使用该对象来校验具体的参数内容。

2.2 拦截器代码分析

看完过滤器代码之后,其实拦截器代码更加简单。整体逻辑上面与过滤器差不多,只不过针对于拦截器,它是通过一个 MethodInvocationValidator 对象来校验传入的参数内容。

public class ValidationInterceptor : IInterceptor
{// Ioc 解析器,用于解析各种注入的组件private readonly IIocResolver _iocResolver;public ValidationInterceptor(IIocResolver iocResolver){_iocResolver = iocResolver;}public void Intercept(IInvocation invocation){// 判断过滤器是否已经处理过if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Validation)){// 处理过则直接进入具体方法内部,执行业务逻辑invocation.Proceed();return;}// 解析出方法验证器,传入请求上下文,并且调用这些验证器具体的验证方法using (var validator = _iocResolver.ResolveAsDisposable<MethodInvocationValidator>()){validator.Object.Initialize(invocation.MethodInvocationTarget, invocation.Arguments);validator.Object.Validate();}invocation.Proceed();}
}

可以看到两个过滤器与拦截器业务逻辑相似,但都是通过验证器来进行处理的,那么验证器又是个什么鬼东西呢?

2.3 参数验证器

验证器即是用来具体执行验证逻辑的工具,从上述代码里面我们可以看到过滤器和拦截器都是通过解析出 MethodInvocationValidator/MvcActionInvocationValidator 之后调用其验证方法进行验证的。

首先我们来看一下 MVC 的验证器是如何进行处理的,看方法类型的定义,可以看到其继承了一个基类,叫 ActionInvocationValidatorBase,而这个基类呢,又继承自 MethodInvocationValidator

public class MvcActionInvocationValidator : ActionInvocationValidatorBase
{// ... 其他代码
}
public abstract class ActionInvocationValidatorBase : MethodInvocationValidator
{// ... 其他代码
}

所以我们分析代码的顺序调整一下,先看一下 MethodInvocationValidator 的内部是如何做处理的吧,这个类型内部还是比较简单的,可能除了有一个递归有点绕之外。

其主要功能就是拿着传递进来的参数值,通过在 Abp 框架启动的时候注入的具体验证器(用户自定义验证器)来递归校验每个参数的值。

/// <summary>
/// 本类用于需要参数验证的方法.
/// </summary>
public class MethodInvocationValidator : ITransientDependency
{// 最大迭代验证次数private const int MaxRecursiveParameterValidationDepth = 8;// 待验证的方法信息protected MethodInfo Method { get; private set; }// 传入的参数值protected object[] ParameterValues { get; private set; }// 方法参数信息protected ParameterInfo[] Parameters { get; private set; }protected List<ValidationResult> ValidationErrors { get; }protected List<IShouldNormalize> ObjectsToBeNormalized { get; }private readonly IValidationConfiguration _configuration;private readonly IIocResolver _iocResolver;public MethodInvocationValidator(IValidationConfiguration configuration, IIocResolver iocResolver){_configuration = configuration;_iocResolver = iocResolver;ValidationErrors = new List<ValidationResult>();ObjectsToBeNormalized = new List<IShouldNormalize>();}// 初始化拦截器参数public virtual void Initialize(MethodInfo method, object[] parameterValues){Check.NotNull(method, nameof(method));Check.NotNull(parameterValues, nameof(parameterValues));Method = method;ParameterValues = parameterValues;Parameters = method.GetParameters();}// 开始验证参数的有效性public void Validate(){// 检测是否初始化,没有初始化则抛出系统级异常CheckInitialized();// 检测方法是否有参数if (Parameters.IsNullOrEmpty()){return;}// 检测方法是否为公开方法if (!Method.IsPublic){return;}// 如果没有开启方法参数检测,则直接返回if (IsValidationDisabled()){return;                }// 如果方法所定义的参数数量与传入的参数值数量匹配不上,则抛出系统级异常if (Parameters.Length != ParameterValues.Length){throw new Exception("Method parameter count does not match with argument count!");}// 遍历方法的参数列表,使用传入的参数值进行校验for (var i = 0; i < Parameters.Length; i++){ValidateMethodParameter(Parameters[i], ParameterValues[i]);}// 如果校验的错误结果集合有任意一条数据,则抛出用户异常,返回给前端展示if (ValidationErrors.Any()){ThrowValidationError();}foreach (var objectToBeNormalized in ObjectsToBeNormalized){objectToBeNormalized.Normalize();}}// ... 忽略的代码// 校验调用方法时传递的参数与参数值protected virtual void ValidateMethodParameter(ParameterInfo parameterInfo, object parameterValue){// 如果参数值为空的情况下,做一系列特殊判断if (parameterValue == null){if (!parameterInfo.IsOptional && !parameterInfo.IsOut && !TypeHelper.IsPrimitiveExtendedIncludingNullable(parameterInfo.ParameterType, includeEnums: true)){ValidationErrors.Add(new ValidationResult(parameterInfo.Name + " is null!", new[] { parameterInfo.Name }));}return;}// 递归校验参数ValidateObjectRecursively(parameterValue, 1);}protected virtual void ValidateObjectRecursively(object validatingObject, int currentDepth){// 验证层级是否超过了最大层级(8)if (currentDepth > MaxRecursiveParameterValidationDepth){return;}// 值是否为空,为空则不继续进行校验if (validatingObject == null){return;}// 判断其类型是否是用户配置的忽略类型,忽略则不进行校验if (_configuration.IgnoredTypes.Any(t => t.IsInstanceOfType(validatingObject))){return;}// 判断参数类型是否为基本类型if (TypeHelper.IsPrimitiveExtendedIncludingNullable(validatingObject.GetType())){return;}SetValidationErrors(validatingObject);// 判定参数类型是否实现了 IEnumerabe 接口,如果实现了,则递归遍历校验其内部的元素if (IsEnumerable(validatingObject)){foreach (var item in (IEnumerable) validatingObject){ValidateObjectRecursively(item, currentDepth + 1);}}// 如果实现了标准化接口,则进行标准化操作if (validatingObject is IShouldNormalize){ObjectsToBeNormalized.Add(validatingObject as IShouldNormalize);}// 是否还需要继续递归校验if (ShouldMakeDeepValidation(validatingObject)){var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();foreach (var property in properties){// 如果有禁止校验的特性则忽略if (property.Attributes.OfType<DisableValidationAttribute>().Any()){continue;}ValidateObjectRecursively(property.GetValue(validatingObject), currentDepth + 1);}}}// ... 其他代码protected virtual bool ShouldValidateUsingValidator(object validatingObject, Type validatorType){return true;}// 是否进行深度验证protected virtual bool ShouldMakeDeepValidation(object validatingObject){// 不需要递归集合对象if (validatingObject is IEnumerable){return false;}var validatingObjectType = validatingObject.GetType();// 不需要递归基础类型的对象if (TypeHelper.IsPrimitiveExtendedIncludingNullable(validatingObjectType)){return false;}return true;}// ... 其他代码
}

有朋友可能会奇怪,在方法内部不是通过 IEnumerable 判断之后来进行递归校验么,为什么在最后面还有一个深度验证呢?

这是因为当前对象除了是一个集合的情况之外,还有可能其内部某个对象是另外一个用户所自定义的复杂对象,这个时候就必须要通过深度验证来校验各个参数的值。不过这个递归也是有限度的,通过 MaxRecursiveParameterValidationDepth 来控制这个迭代层数为 8 层。如果不加以限制的话,那么很有可能出现循环引用而产生死循环的情况,或者是层级过深导致接口相应缓慢。

那么在这里执行具体校验操作的则是那些实现了 IMethodParameterValidator 接口的对象,这些对象在 Abp 核心模块(AbpKernelModule)的预加载的时候被添加到了 Configuration.Validation.Validators 属性当中。

当然用户也可以在自己的模块预加载方法当中增加自己的参数验证器,只要实现该接口即可。

public sealed class AbpKernelModule : AbpModule
{public override void PreInitialize(){// ... 其他代码// 增加需要忽略的类型AddIgnoredTypes();// 增加参数校验器AddMethodParameterValidators();}private void AddMethodParameterValidators(){Configuration.Validation.Validators.Add<DataAnnotationsValidator>();Configuration.Validation.Validators.Add<ValidatableObjectValidator>();Configuration.Validation.Validators.Add<CustomValidator>();}// Abp 默认需要忽略的对象private void AddIgnoredTypes(){var commonIgnoredTypes = new[]{typeof(Stream),typeof(Expression)};foreach (var ignoredType in commonIgnoredTypes){Configuration.Auditing.IgnoredTypes.AddIfNotContains(ignoredType);Configuration.Validation.IgnoredTypes.AddIfNotContains(ignoredType);}var validationIgnoredTypes = new[] { typeof(Type) };foreach (var ignoredType in validationIgnoredTypes){Configuration.Validation.IgnoredTypes.AddIfNotContains(ignoredType);}}
}

之后呢,回到之前的校验方法,可以看到在 SetValidationErrors(object validatingObject) 方法里面遍历了之前被注入的验证器集合,然后调用其 Validate() 方法来进行具体的参数校验。

protected virtual void SetValidationErrors(object validatingObject)
{foreach (var validatorType in _configuration.Validators){if (ShouldValidateUsingValidator(validatingObject, validatorType)){using (var validator = _iocResolver.ResolveAsDisposable<IMethodParameterValidator>(validatorType)){var validationResults = validator.Object.Validate(validatingObject);ValidationErrors.AddRange(validationResults);}}}
}

2.4 具体的参数验证器

这里以 Abp 默认实现的 DataAnnotationValidator 类型为例,可以看看他是怎么来根据参数的数据注解来验证参数是否正确的。

public class DataAnnotationsValidator : IMethodParameterValidator
{public virtual IReadOnlyList<ValidationResult> Validate(object validatingObject){return GetDataAnnotationAttributeErrors(validatingObject);}protected virtual List<ValidationResult> GetDataAnnotationAttributeErrors(object validatingObject){var validationErrors = new List<ValidationResult>();var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();// 获得参数值的所有属性,如果传入的是一个 DTO 对象的话,他内部肯定会有很多属性的foreach (var property in properties){var validationAttributes = property.Attributes.OfType<ValidationAttribute>().ToArray();// 没有数据注解特性,跳过当前属性处理if (validationAttributes.IsNullOrEmpty()){continue;}// 创建一个错误信息上下文,用户数据注解工具进行校验var validationContext = new ValidationContext(validatingObject){DisplayName = property.DisplayName,MemberName = property.Name};// 根据特性来校验参数结果foreach (var attribute in validationAttributes){var result = attribute.GetValidationResult(property.GetValue(validatingObject), validationContext);if (result != null){validationErrors.Add(result);}}}return validationErrors;}
}

3. 后记

最近工作较忙,可能更新速度不会像原来那么快,不过我尽可能在国庆结束后完成剩余文章,谢谢大家的支持。

作者:myzony

出处:https://www.cnblogs.com/myzony/p/9716742.html

公众号“码侠江湖”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!

扫描二维码

获取更多精彩

码侠江湖

喜欢就点个在看再走吧

[Abp 源码分析]DTO 自动验证相关推荐

  1. 【转】ABP源码分析十七:DTO 自动校验的实现

    对传给Application service对象中的方法的DTO参数,ABP都会在方法真正执行前自动完成validation(根据标注到DTO对象中的validate规则). ABP是如何做到的? 思 ...

  2. [Abp 源码分析]权限验证

    点击上方蓝字关注我们 0.简介 Abp 本身集成了一套权限验证体系,通过 ASP.NET Core 的过滤器与 Castle 的拦截器进行拦截请求,并进行权限验证.在 Abp 框架内部,权限分为两块, ...

  3. [Abp 源码分析]多租户体系与权限验证

    点击上方蓝字关注我们 0.简介 承接上篇文章我们会在这篇文章详细解说一下 Abp 是如何结合 IPermissionChecker 与 IFeatureChecker 来实现一个完整的多租户系统的权限 ...

  4. 【转】ABP源码分析十六:DTO的设计

    IDTO:空接口,用于标注Dto对象. ComboboxItemDto:用于combobox/list中Item的DTO NameValueDto<T>/NameValueDto:用于na ...

  5. [Abp 源码分析]ASP.NET Core 集成

    点击上方蓝字关注我们 0. 简介 整个 Abp 框架最为核心的除了 Abp 库之外,其次就是 Abp.AspNetCore 库了.虽然 Abp 本身是可以用于控制台程序的,不过那样的话 Abp 就基本 ...

  6. 【转】ABP源码分析一:整体项目结构及目录

    ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...

  7. 【转】ABP源码分析四十七:ABP中的异常处理

    ABP 中异常处理的思路是很清晰的.一共五种类型的异常类. AbpInitializationException用于封装ABP初始化过程中出现的异常,只要抛出AbpInitializationExce ...

  8. 【转】ABP源码分析三十六:ABP.Web.Api

    这里的内容和ABP 动态webapi没有关系.除了动态webapi,ABP必然是支持使用传统的webApi.ABP.Web.Api模块中实现了一些同意的基础功能,以方便我们创建和使用asp.net w ...

  9. 【转】ABP源码分析三十五:ABP中动态WebAPI原理解析

    动态WebAPI应该算是ABP中最Magic的功能之一了吧.开发人员无须定义继承自ApiController的类,只须重用Application Service中的类就可以对外提供WebAPI的功能, ...

最新文章

  1. 毕业仅1年,干Python赚了50W 网友:不是吹的
  2. CDN监控系统(一)
  3. PDF文件上载图标,与启用浏览器浏览允许后依然无法在浏览器打开PDF文件的解决方案...
  4. linux7.3怎么修改时区,CentOS7校准核对服务器时区和手动修改时间
  5. MinGW和MSYS的自动安装 【转】
  6. d3.js 获取当前像素坐标_Cesium开发入门篇 | 06坐标系及坐标变换
  7. 在安装keepalived出现问题:需要:libmysqlclient.so.18
  8. cenos6.4安装gvim
  9. 苹果付费app共享公众号_娄底共享云店铺公众号
  10. 【云服务月刊】2018年第7期:云栖大会门票免费送!阿里云MVP招募,就等你了!...
  11. HOWTO:InstallShield中如何通过脚本获取“My Documents”路径
  12. HTML5期末大作业:学校网站设计——初级中学网站(16页)HTML+CSS+JavaScript 学校网页设计作业 网页设计作业 学生个人设计成品,网页设计作业
  13. linux操作系统学习心得
  14. Windows事件ID详细
  15. Android 播放器 mov,Android 调用系统播放器
  16. Zoommy for mac(图片素材搜索下载软件)
  17. 多维尺度分析(Multidimensional scaling,MDS)及SPSS实现
  18. Leetcode刷题笔记12:【20】有效的括号【155】最小栈【255】用队列实现栈(STL:stackC++ 单引号和双引号)
  19. 【笔记】创新思维工作坊(一)
  20. 第一章 Java简介

热门文章

  1. Centos编译安装Apache 2.4.6笔记 配置
  2. JCheckbox全选
  3. Hello, AnnsShadow!
  4. AMD and CMD are dead之KMD.js依赖可视化工具发布
  5. sdut2784cf 126b Good Luck!(next数组)
  6. 特定视图呈现时发生的事件顺序
  7. SQL Server索引进阶第十篇:索引的内部结构
  8. 【转】SQL SERVER 存储过程学习笔记
  9. WinXP中鲜为人知的28项隐藏功能
  10. twitter api使用_使用P2创建自己的Twitter风格的组博客