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 MVC
services.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. 后记

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

4.点此跳转到总目录

[Abp 源码分析]十四、DTO 自动验证的更多相关文章

  1. C# DateTime的11种构造函数 [Abp 源码分析]十五、自动审计记录 .Net 登陆的时候添加验证码 使用Topshelf开发Windows服务、记录日志 日常杂记——C#验证码 c#_生成图片式验证码 C# 利用SharpZipLib生成压缩包 Sql2012如何将远程服务器数据库及表、表结构、表数据导入本地数据库

    C# DateTime的11种构造函数   别的也不多说没直接贴代码 using System; using System.Collections.Generic; using System.Glob ...

  2. ABP源码分析十四:Entity的设计

    IEntity<TPrimaryKey>: 封装了PrimaryKey:Id,这是一个泛型类型 IEntity: 封装了PrimaryKey:Id,这是一个int类型 Entity< ...

  3. [Abp 源码分析]十五、自动审计记录

    0.简介 Abp 框架为我们自带了审计日志功能,审计日志可以方便地查看每次请求接口所耗的时间,能够帮助我们快速定位到某些性能有问题的接口.除此之外,审计日志信息还包含有每次调用接口时客户端请求的参数信 ...

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

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

  5. ABP源码分析十七:DTO 自动校验的实现

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

  6. ABP源码分析十九:Auditing

    审计跟踪(也叫审计日志)是与安全相关的按照时间顺序的记录,它们提供了活动序列的文档证据,这些活动序列可以在任何时间影响一个特定的操作. AuditInfo:定义如下图中需要被Audit的信息. Aud ...

  7. ABP源码分析十:Unit Of Work

    ABP以AOP的方式实现UnitOfWork功能.通过UnitOfWorkRegistrar将UnitOfWorkInterceptor在某个类被注册到IOCContainner的时候,一并添加到该类 ...

  8. ABP源码分析十二:本地化

    本文逐个分析ABP中涉及到locaization的接口和类,以及相互之间的关系.本地化主要涉及两个方面:一个是语言(Language)的管理,这部分相对简单.另一个是语言对应得本地化资源(Locali ...

  9. ABP源码分析十五:ABP中的实用扩展方法

    类名 扩展的类型 方法名 参数 作用 XmlNodeExtensions XmlNode GetAttributeValueOrNull attributeName Gets an   attribu ...

随机推荐

  1. 阿里云+WordPress搭建个人博客

    搭建过程: 第一步:首先你需要一台阿里云服务器ECS,如果你是学生,可以享受学生价9.5元/月 (阿里云翼计划:https://promotion.aliyun.com/ntms/act/campus ...

  2. Java排序 - 不实用的几个排序算法 -- 睡眠排序、猴子排序、面条排序、珠排序

    介绍几个不实用的排序算法,一来可以在学习时增加一些乐趣,放松一下自己,二来可以学习一下.思考一下这些算法失败在哪里,又是否存在一些好的地方? 睡眠排序 这是一个思想比较简单,脑洞巨大的算法 -- 我们 ...

  3. 2018-2019-2 20165239 《网络对抗技术》Kali的安装 第一周

    2018-2019-<网络对抗技术> Kali安装 20165239其米仁增 一.资源下载以及工具安装 1.下载虚拟机工具VMware. 下载链接 :https://www.baidu.c ...

  4. sklearn.datasates 加载测试数据

    数据一:波士顿房价(适合做回归),以后直接用boston标记 这行代码就读进来了boston = sklearn.datasets.load_boston()查询具体数据说明,用这个代码:print ...

  5. Linux-网络基础

    1.如何将俩台不同网络中的电脑通信,见下图

  6. navigator的一些冷知识

    { 监听屏幕旋转变化接口: orientationchange orientation.angle : 0 竖屏 , 90 向左横屏 , -90/270 向右横屏 , 180 倒屏 } screenO ...

  7. CentOS7配置mailx使用外部smtp服务器发送邮件

    转自huskiesir的博客: 发送邮件的两种方式: 1.连接现成的smtp服务器去发送(此方法比较简单,直接利用现有的smtp服务器比如qq.新浪.网易等邮箱,只需要直接配置mail.rc文件即可实 ...

  8. Vue使用过程中常见问题

    目录 一.vue监听不到state数组/json对象内的元素的值的变化,要手动通知触发 二.vue用splice删除多维数组元素导致视图更新失败情况 三.vue项目如何部署到php或者java环境的服 ...

  9. git创建本地分支以及推送本地分之至远程分支

    Git分支策略 实际开发中,应当按照以下几个基本原则进行管理: 首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能再上边干活. 那在哪干活呢?干活都在dev分支上,也就是说,de ...

  10. PostgreSQL>窗口函数的用法

    PostgreSQL之窗口函数的用法 转载请注明出处:https://www.cnblogs.com/funnyzpc/p/9311281.html PostgreSQL的高级特性本准备三篇的(递归. ...