Abp 源码分析:十四、DTO 自动验证

0.简介

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

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

1.启动流程

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

1.1 过滤器注入

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private static void ConfigureAspNetCore(IServiceCollection services, IIocResolver iocResolver)
{
    // ... 其他代码
    
    //Configure MVC
    services.Configure<MvcOptions>(mvcOptions =>
    {
        mvcOptions.AddAbp(services);
    });
    
    // ... 其他代码
}

过滤器注入方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 来针对每次注入的应用服务应用上参数验证拦截器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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 接口,也就是说当每次接口被调用的时候都会进入该拦截器的内部。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 的参数验证功能,禁用了则不进行任何操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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() 方法内部会有这样一句代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void Intercept(IInvocation invocation)
{
    // 判断是否拥有处理过的标识
    if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Validation))
    {
        invocation.Proceed();
        return;
    }

    // ... 其他代码
}

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

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

await next();

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

2.2 拦截器代码分析

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

1
2
3
4
5
6
7
8
public class MvcActionInvocationValidator : ActionInvocationValidatorBase
{
    // ... 其他代码
}
public abstract class ActionInvocationValidatorBase : MethodInvocationValidator
{
	// ... 其他代码
}

所以我们分析代码的顺序调整一下,先看一下 MethodInvocationValidator 的内部是如何做处理的吧。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
/// <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 CheckInitialized()
    {
        if (Method == null)
        {
            throw new AbpException("This object has not been initialized. Call Initialize method first.");
        }
    }

    protected virtual bool IsValidationDisabled()
    {
        if (Method.IsDefined(typeof(EnableValidationAttribute), true))
        {
            return false;
        }

        return ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault<DisableValidationAttribute>(Method) != null;
    }

    protected virtual void ThrowValidationError()
    {
        throw new AbpValidationException(
            "Method arguments are not valid! See ValidationErrors for details.",
            ValidationErrors
        );
    }

    /// <summary>
    /// Validates given parameter for given value.
    /// </summary>
    /// <param name="parameterInfo">Parameter of the method to validate</param>
    /// <param name="parameterValue">Value to validate</param>
    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)
    {
        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);

        // Validate items of enumerable
        if (IsEnumerable(validatingObject))
        {
            foreach (var item in (IEnumerable) validatingObject)
            {
                ValidateObjectRecursively(item, currentDepth + 1);
            }
        }

        // Add list to be normalized later
        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 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);
                }
            }
        }
    }

    protected virtual bool ShouldValidateUsingValidator(object validatingObject, Type validatorType)
    {
        return true;
    }

    protected virtual bool ShouldMakeDeepValidation(object validatingObject)
    {
        // Do not recursively validate for enumerable objects
        if (validatingObject is IEnumerable)
        {
            return false;
        }

        var validatingObjectType = validatingObject.GetType();

        // Do not recursively validate for primitive objects
        if (TypeHelper.IsPrimitiveExtendedIncludingNullable(validatingObjectType))
        {
            return false;
        }

        return true;
    }

    private bool IsEnumerable(object validatingObject)
    {
        return
            validatingObject is IEnumerable &&
            !(validatingObject is IQueryable) &&
            !TypeHelper.IsPrimitiveExtendedIncludingNullable(validatingObject.GetType());
    }
}

3. 后记

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

Built with Hugo
主题 StackJimmy 设计