myzony

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() 方法用于添加各种拦截器的注册类实例。代码如下:

来到 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. 后记

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

相关文章: