Asp.net MVC 全局字符串参数过滤前后空白字符

小柊 发表于 2017年06月25日 21时15分27秒

序、扯淡

又是在不知不觉之中,一个月就这么过去了。这一个月里发生了挺多的事情,比如说大学毕业(虽然辣鸡学校扣了毕业证和学位证,最后只能申请延期毕业,这里的大学毕业仅仅是参加了毕业仪式罢了);工作一直不能稳定下来,现在的公司部门编制满了,如果需要转正就需要换部门,要么就直接换工作(我现在越来越偏向后者了);在这六月里,杭州也进入了梅雨季节,连绵不断的阴雨天气搞的整个人都没有心情和精神;拖欠辅导员的大学感悟一拖就是两个月,现在还在绝赞拖稿中……

唉……

 

一、背景

今天我们还是以一个案例切入,讲解一下Asp.net MVC中筛选器的实际使用。

空格这个东西真是让人又爱又恨,你说离了它不行,多了它也要出事儿。最近自己在开发自用的一套系统里,一次重置密码操作后,发现新密码怎么都登陆不上去,查了后台数据库,发现密码确实已经更新了,但具体密码被hash过了,看不到当时设置的新密码,后来突然想起现在的系统里已经布满了我的日志钩子,查了一下操作日志才发现当时提交新密码的时候手滑在密码最后加了一个空格……

为了在实际上线的时候不让用户发生这种问题,我们可以在使用参数前,先调用字符串参数的Trim()方法,去清除字符串变量前后的空字符串。不过随着项目越来越大,提交参数的地方越来越多,在每次使用参数前调用Trim()方法的做法明显不够优雅(我个人的编程优雅之道就是“用最少的代码做最多的事”)。那优雅的方法是怎么做的呢?

 

二、筛选器(Filter)

其实关于Asp.net MVC的筛选器我之前在另一篇博客《C# 全局异常捕获》里简单提到过,当时就只简单的提了一下Asp.net MVC里,一共有4中基本筛选器,分别是:

分类 接口 默认实现 运行时间
Authorization IAuthorizationFilter AuthorizeAttribute 在Action方法之前和其它类型的Filter之前运行。
Action IActionFilter ActionFilterAttribute 在Action方法之前运行。
Result IResultFilter ActionFilterAttribute 在处理ActionResult之前或之后运行。
Exception IExceptionFilter HandleErrorAttribute 在Action方法、ActionResult和其他类型的Filter抛出异常时运行。

 

然后那篇文章就直接讲了怎么用IExceptionFilter全局拦截异常,然后就结束了。

现在看看有点不负责任,就随便写了一下代码,没有具体的细讲,所以今天上代码之前,请允许我简单的讲解一下筛选器这一功能。首先我们来看一下Asp.net MVC的处理管道模型图(图转互联网):

 

筛选器(Filter)这个概念是在Asp.net MVC中独有的,在WebForm中并不存在。我们从上面的处理管道模型上可以看到,除了在Controller Initialization过程中出现的Http Handler这个Asp.net WebForm时代就出现模块以外,在整个管道的不同位置上都分布着不同类型的Filter。

有人说,Filter(筛选器)是基于AOP(面向切面编程)的设计,它的作用是对MVC框架处理客户端请求注入额外的逻辑,以非常简单优美的方式实现横切关注点(Cross-cutting Concerns)。横切关注点是指横越应该程序的多个甚至所有模块的功能,经典的横切关注点有日志记录、缓存处理、异常处理和权限验证等。

也正是因为Filter,利用分布在整个Asp.net MVC处理管道中不同部位的筛选器,我们可以针对性的将代码注入到指定位置,对HTTP请求或响应进行记录或修改甚至阻断,并不影响各个部件间的耦合程度。简单,优雅。

 

三、代码实现

其实从刚刚上面的管道图上我们可以发现,我们只要能赶在请求进入Action Execution环节之前,将请求中的字符串参数执行Trim()就可以了。在Action Execution环节,我们能注入代码的地方有:Http Handler、Authentication Filter、Authorization Filter和Action Filter这四个环节。

首先是Http Handler:这个部分是延续Asp.net WebForm的风格下来的。我个人在学习.net Web开发的时候是直接跳过Asp.net WebForm直接上MVC的,所以对它内部原理不是特别了解。但如果只是完成今天这个题目的要求的话,是可以用Http Module来实现的 ,但我个人并不是特别推荐使用它,主要是因为它太笨重,除了继承接口以外,还要修改Web.config进行注册。且如果下次需要拦截响应内容的话,它就没法用了,所以在Http Module和Filter之间,我选择了Filter。

然后是Authentication Filter和Authorization Filter这两个筛选器,这两个筛选器一看名字就知道是关于身份验证和身份授权的,并不适用本次题目的要求。

所以剩下来的就只有Action Filter了。我们来简单的看一下Action Filter的接口IActionFilter:

 

它只声明了两个方法:

void OnActionExecuting(ActionExecutingContext filterContext);
void OnActionExecuted(ActionExecutedContext filterContext);

 

前者在Action方法被执行之前触发,后者在Action方法执行完成后,准备输出结果时触发。

 

好的,我们现在新建一个类,类名叫做AuthorizeFilter:

 

然后使此类继承IActionFilter接口,AuthorizeFilter类具体代码如下:

/// <summary>
/// 全局参数空白字符剔除过滤器
/// </summary>
public class TrimFilter : IActionFilter
{
	#region 在执行操作方法后调用 + public void OnActionExecuted(ActionExecutedContext filterContext)
	/// <summary>
	/// 在执行操作方法后调用
	/// </summary>
	/// <param name="filterContext">筛选器上下文</param>
	public void OnActionExecuted(ActionExecutedContext filterContext)
	{
		//不需要,留空
	}
	#endregion
	
	#region 在执行操作方法之前调用 + public void OnActionExecuting(ActionExecutingContext filterContext)
	/// <summary>
	/// 在执行操作方法之前调用
	/// </summary>
	/// <param name="filterContext">筛选器上下文</param>
	public void OnActionExecuting(ActionExecutingContext filterContext)
	{
		ParameterDescriptor[] parameters = filterContext.ActionDescriptor.GetParameters();
		
		foreach (ParameterDescriptor parameter in parameters)
		{
			if (parameter.ParameterType == typeof(string))
			{
				filterContext.ActionParameters[parameter.ParameterName] = (filterContext.ActionParameters[parameter.ParameterName] as string)?.Trim();
			}
		}
	}
	#endregion
}

 

注:以上代码需要另外引用System.Web.Mvc命名空间。

 

完成后,还需要对此筛选器进行代码注册,步骤非常简单,在MVC项目下的“App_Start”文件夹下有一个“FilterConfig.cs”类,进入后会有一个RegisterGlobalFilters(GlobalFilterCollection filters)方法,在此方法中新增一行代码:

filters.Add(new TrimFilter());

 

即可。

刚刚我们也提到过,我们只需要在参数进入Action方法前修改一下参数就好,所以对我们来说,OnActionExecuted这个方法对我们并没有什么作用,但因为需要实现接口定义的所有方法,所以不能删去,只能留空。

我们主要的代码就在OnActionExecuting这个方法中,我简单的讲解一下这个方法里都做了什么:

首先第一行代码取出当前HTTP请求所对应的Action方法所有参数,放入parameters数组中,然后使用foreach循环,遍历parameters数组,依次判断数组中各元素是否为字符串类型,如果是的话使用as转换为字符串类型并执行Trim()方法。

 

需要注意的是,有可能会出现变量值为null的情况,所以在这里我使用的C# 6新增的空操作符(?.)以防止出现NullReferenceException异常。Visual Studio 2015以下的VS可能不能识别此操作符,需要稍作修改,具体自行百度,本人就不在此啰嗦了。

我们稍微看一下效果吧!

我们首先找个登录页,故意在用户名后面加上一个空格,点击登陆查看效果。

 

我们在OnActionExecuting方法的开始处和结尾处设下断点,查看变量内容:

筛选器拦截到请求:

 

可以看到在这里username变量后方还是有我们故意输进去的空格的。

 

方法结束时:

 

可以看到,在方法结束的时候,username变量已经没有了后面的空格,多余的空格已被去掉了。继续执行,让请求进入Action方法,观察Action接收到的参数值:

完美,参数中多余的空格成功的被去除了。

 

四、深入优化

刚刚在第三章给出的筛选器代码,可以对Action方法中所有字符串变量进行前后空白字符的清理,不过我们并不是什么时候都会用字符串接收参数,我们很多时候都会将参数写成一个模型类,交由Asp.net MVC进行模型绑定。

比如我下面这个方法,就用到了模型绑定:

 

使用到的模型ResetPwd,具体定义如下:

 

那我们刚刚的筛选器能对这种使用了模型绑定的Action进行参数过滤吗?

我们来试一下好了。

筛选器方法进入时:

 

筛选器方法结束时:

 

我们可以看到在这种情况下,虽然筛选器被执行了,但清理并没有生效。

 

为什么这个筛选器不能对进行模型绑定的Action方法进行过滤呢?原因其实很简单,之前上面的代码里,我们可以看到这样一句判断语句:

if (parameter.ParameterType == typeof(string))
{
	filterContext.ActionParameters[parameter.ParameterName] = (filterContext.ActionParameters[parameter.ParameterName] as string)?.Trim();
}

 

这句判断语句将判断此Action方法参数是否为字符串类型,我们在这里使用了模型绑定,所有的数据都被绑定到了ResetPwd类中,而ResetPwd方法是不能通过上面的条件判断的,因为它的类型明显不是字符串。

 

那么就没有办法了吗?

当然还有啊,这位朋友,您听说过安利反射吗?。

 

简单的讲一下思路:

我们可以通过反射取得类型的所有公开(public)属性,如果类型的某公开属性既是字符串(string)类型,也是可读可写的。那就取出此属性的值,进行Trim()操作。然后重新放回。

但是反射会浪费性能,所以我们还需要设置一个开关,去控制筛选器要不要去反射参数,以免筛选器对所有的请求都进行反射造成不必要的性能浪费。

接下来上代码,和上一章的代码差不多,除了需要引用System.Web.Mvc这命名空间以外,这次还要再多引用一个System.Reflection命名空间,由于使用了C# 6新增的空操作符(?.),Visual Studio 2015以下版本需要手动调整代码。

/// <summary>
/// 全局参数空白字符剔除过滤器
/// </summary>
public class TrimFilter : IActionFilter
{
	#region 在执行操作方法后调用 + public void OnActionExecuted(ActionExecutedContext filterContext)
	/// <summary>
	/// 在执行操作方法后调用
	/// </summary>
	/// <param name="filterContext">筛选器上下文</param>
	public void OnActionExecuted(ActionExecutedContext filterContext)
	{
		//不需要,留空
	}
	#endregion
	
	#region 在执行操作方法之前调用 + public void OnActionExecuting(ActionExecutingContext filterContext)
	/// <summary>
	/// 在执行操作方法之前调用
	/// </summary>
	/// <param name="filterContext">筛选器上下文</param>
	public void OnActionExecuting(ActionExecutingContext filterContext)
	{
		//深度清理标志变量
		bool trimFlag = this.HasDescriptor<ParameterTrimAttribute>(filterContext);
		ParameterDescriptor[] parameters = filterContext.ActionDescriptor.GetParameters();
		
		foreach (ParameterDescriptor parameter in parameters)
		{
			object value = filterContext.ActionParameters[parameter.ParameterName];
			if (value == null)
			{
				continue;
			}
			else if (parameter.ParameterType == typeof(string))
			{
				filterContext.ActionParameters[parameter.ParameterName] = (value as string)?.Trim();
			}
			else if (trimFlag)
			{
				filterContext.ActionParameters[parameter.ParameterName] = this.ParameterCleaner(value);
			}
		}
	}
	#endregion

	#region 参数清理器 + private object ParameterCleaner(object obj)
	/// <summary>
	/// 参数清理器
	/// </summary>
	/// <param name="obj">待清理对象</param>
	/// <returns>已清理对象</returns>
	private object ParameterCleaner(object obj)
	{
		Type type = obj.GetType();
		if (!type.IsClass) return obj;  //不是类的话直接返回
		
		PropertyInfo[] properties = type.GetProperties();
		foreach (var property in properties)
		{
			//判断属性是否为字符串类型且可读可写
			if ((property.PropertyType == typeof(string)) && property.CanRead && property.CanWrite)
			{
				string value = property.GetValue(obj, null) as string;
				property.SetValue(obj, value?.Trim(), null);
			}
		}

		return obj;
	}
	#endregion
	
	#region 判断筛选器上下文中是否存在指定标签 + private bool HasDescriptor<T>(ActionExecutingContext filterContext) where T : Attribute
	/// <summary>
	/// 判断筛选器上下文中是否存在指定标签
	/// </summary>
	/// <typeparam name="T">待判断标签类型</typeparam>
	/// <param name="filterContext">权限验证筛选器上下文</param>
	/// <returns>判断结果</returns>
	private bool HasDescriptor<T>(ActionExecutingContext filterContext) where T : Attribute
	{
		if (filterContext.ActionDescriptor.IsDefined(typeof(T), false) || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(T), false))
			return true;
		else
			return false;
	}
	#endregion
}

/// <summary>
/// 强制清除参数前后空格标签
/// <para>本系统默认清除所有请求参数中字符串类型的前后空白字符。</para>
/// <para>但对于那些不是字符串类型但内部属性中包含字符串类型的参数,系统默认进行不清理。</para>
/// <para>如果需要对这些类型的内部属性进行清理,请在控制器或方法上加上此标签。</para>
/// <para>注:即便加上此标签,也仅对公开的可读可写的属性进行清理。</para>
/// </summary>
public class ParameterTrimAttribute : Attribute { }

 

上面的代码使用ParameterTrimAttribute标签控制筛选器是否进行反射过滤。例如刚刚的方法中,我们只需要在方法上方加上

[ParameterTrim]

 

特性标签即可,如下:

 

修改代码后再次尝试提交。

筛选器方法进入时:

 

筛选器方法结束时:

 

Action方法进入时:

 

完美。

 

五、结束语

本篇博客主要以清除请求中字符串参数前后的空白字符为例,简单讲述了筛选器(Filter)的使用。筛选器简单轻便,基于AOP的设计,可以在项目中简单的进行数据拦截操作。

如果您也有类似的使用需求,不妨试试筛选器,相信它一定不会让您失望的。

 

 

 

 

 

小柊

2017年6月25日 21:10:56

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注