一、过滤器(Filter)
ASP.NET MVC中的每一个请求,都会分配给对应Controller(以下简称“控制器”)下的特定Action(以下简称“方法”)处理,正常情况下直接在方法里写代码就可以了,但是如果想在方法执行之前或者之后处理一些逻辑,这里就需要用到过滤器。
常用的过滤器有三个:Authorize(授权过滤器),HandleError(异常过滤器),ActionFilter(自定义过滤器),对应的类分别是:AuthorizeAttribute、HandleErrorAttribute和ActionFilterAttribute,继承这些类并重写其中方法即可实现不同的功能。
1.Authorize授权过滤器
授权过滤器顾名思义就是授权用的,授权过滤器在方法执行之前执行,用于限制请求能不能进入这个方法,新建一个方法:
1
2
3
4
|
public JsonResult AuthorizeFilterTest() { return Json( new ReturnModel_Common { msg = "hello world!" }); } |
直接访问得到结果:
现在假设这个AuthorizeFilterTest方法是一个后台方法,用户必须得有一个有效的令牌(token)才能访问,常规做法是在AuthorizeFilterTest方法里接收并验证token,但是这样一旦方法多了,每个方法里都写验证的代码显然不切实际,这个时候就要用到授权过滤器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public class TokenValidateAttribute : AuthorizeAttribute { /// <summary> /// 授权验证的逻辑处理。返回true则通过授权,false则相反 /// </summary> /// <param name="httpContext"></param> /// <returns></returns> protected override bool AuthorizeCore(HttpContextBase httpContext) { string token = httpContext.Request[ "token" ]; if ( string .IsNullOrEmpty(token)) { return false ; } else { return true ; } } } |
新建了一个继承AuthorizeAttribute的类,并重写了其中的AuthorizeCore方法,这段伪代码实现的就是token有值即返回true,没有则返回false,标注到需要授权才可以访问的方法上面:
1
2
3
4
5
|
[TokenValidate] public JsonResult AuthorizeFilterTest() { return Json( new ReturnModel_Common { msg = "hello world!" }) } |
标注TokenValidate后,AuthorizeCore方法就在AuthorizeFilterTest之前执行,如果AuthorizeCore返回true,那么授权成功执行AuthorizeFilterTest里面的代码,否则授权失败。不传token:
传token:
不传token授权失败时进入了MVC默认的未授权页面。这里做下改进:不管授权是成功还是失败都保证返回值格式一致,方便前端处理,这个时候重写AuthorizeAttribute类里的HandleUnauthorizedRequest方法即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/// <summary> /// 授权失败处理 /// </summary> /// <param name="filterContext"></param> protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { base .HandleUnauthorizedRequest(filterContext); var json = new JsonResult(); json.Data = new ReturnModel_Common { success = false , code = ReturnCode_Interface.Token过期或错误, msg = "token expired or error" }; json.JsonRequestBehavior = JsonRequestBehavior.AllowGet; filterContext.Result = json; } |
效果:
实战:授权过滤器最广泛的应用还是做权限管理系统,用户登录成功后服务端输出一个加密的token,后续的请求都会带上这个token,服务端在AuthorizeCore方法里解开token拿到用户ID,根据用户ID去数据库里查是否有请求当前接口的权限,有就返回true,反之返回false。这种方式做授权,相比登录成功给Cookie和Session的好处就是一个接口PC端、App端共同使用。
2.HandleError异常过滤器
异常过滤器是处理代码异常的,在系统的代码抛错的时候执行,MVC默认已经实现了异常过滤器,并且注册到了App_Start目录下的FilterConfig.cs:
1
|
filters.Add( new HandleErrorAttribute()); |
这个生效于整个系统,任何接口或者页面报错都会执行MVC默认的异常处理,并返回一个默认的报错页面:Views/Shared/Error(程序发到服务器上报错时才可以看到本页面,本地调试权限高,还是可以看到具体报错信息的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@{ Layout = null; } <!DOCTYPE html> < html > < head > < meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" /> < meta name = "viewport" content = "width=device-width" /> < title >错误</ title > </ head > < body > < hgroup > < h1 >错误。</ h1 > < h2 >处理你的请求时出错。</ h2 > </ hgroup > </ body > </ html > |
默认的异常过滤器显然无法满足使用需求,重写下异常过滤器,应付项目实战中的需求:
1)报错可以记录错误代码所在的控制器和方法,以及报错时的请求参数和时间;
2)返回特定格式的JSON方便前端处理。因为现在系统大部分是ajax请求,报错了返回MVC默认的报错页面,前端不好处理
新建一个类LogExceptionAttribute继承HandleErrorAttribute,并重写内部的OnException方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public override void OnException(ExceptionContext filterContext) { if (!filterContext.ExceptionHandled) { string controllerName = ( string )filterContext.RouteData.Values[ "controller" ]; string actionName = ( string )filterContext.RouteData.Values[ "action" ]; string param = Common.GetPostParas(); string ip = HttpContext.Current.Request.UserHostAddress; LogManager.GetLogger( "LogExceptionAttribute" ).Error( "Location:{0}/{1} Param:{2}UserIP:{3} Exception:{4}" , controllerName, actionName, param, ip, filterContext.Exception.Message); filterContext.Result = new JsonResult { Data = new ReturnModel_Common { success = false , code = ReturnCode_Interface.服务端抛错, msg = filterContext.Exception.Message }, JsonRequestBehavior = JsonRequestBehavior.AllowGet }; } if (filterContext.Result is JsonResult) filterContext.ExceptionHandled = true ; //返回结果是JsonResult,则设置异常已处理 else base .OnException(filterContext); //执行基类HandleErrorAttribute的逻辑,转向错误页面 } |
异常过滤器就不像授权过滤器一样标注在方法上面了,直接到App_Start目录下的FilterConfig.cs注册下,这样所有的接口都可以生效了:
1
|
filters.Add( new LogExceptionAttribute()); |
异常过滤器里使用了NLog作为日志记录工具,Nuget安装命令:
1
2
|
Install-Package NLog Install-Package NLog.Config |
相比Log4net,NLog配置简单,仅几行代码即可,NLog.config:
1
2
3
4
5
6
7
8
9
10
|
<? xml version = "1.0" encoding = "utf-8" ?> < nlog xmlns = "http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance" > < targets > < target xsi:type = "File" name = "f" fileName = "${basedir}/log/${shortdate}.log" layout = "${uppercase:${level}} ${longdate} ${message}" /> < target xsi:type = "File" name = "f2" fileName = "D:\log\MVCExtension\${shortdate}.log" layout = "${uppercase:${level}} ${longdate} ${message}" /> </ targets > < rules > < logger name = "*" minlevel = "Debug" writeTo = "f2" /> </ rules > </ nlog > |
如果报错,日志就记录在D盘的log目录下的MVCExtension目录下,一个项目一个日志目录,方便管理。全部配置完成,看下代码:
1
2
3
4
5
|
public JsonResult HandleErrorFilterTest() { int i = int .Parse( "abc" ); return Json( new ReturnModel_Data { data = i }); } |
字符串强转成int类型,必然报错,页面响应:
同时日志也记录下来了:
3.ActionFilter自定义过滤器
自定义过滤器就更加灵活了,可以精确的注入到请求前、请求中和请求后。继承抽象类ActionFilterAttribute并重写里面的方法即可:
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
|
public class SystemLogAttribute : ActionFilterAttribute { public string Operate { get ; set ; } public override void OnActionExecuted(ActionExecutedContext filterContext) { filterContext.HttpContext.Response.Write( "<br/>" + Operate + ":OnActionExecuted" ); base .OnActionExecuted(filterContext); } public override void OnActionExecuting(ActionExecutingContext filterContext) { filterContext.HttpContext.Response.Write( "<br/>" + Operate + ":OnActionExecuting" ); base .OnActionExecuting(filterContext); } public override void OnResultExecuted(ResultExecutedContext filterContext) { filterContext.HttpContext.Response.Write( "<br/>" + Operate + ":OnResultExecuted" ); base .OnResultExecuted(filterContext); } public override void OnResultExecuting(ResultExecutingContext filterContext) { filterContext.HttpContext.Response.Write( "<br/>" + Operate + ":OnResultExecuting" ); base .OnResultExecuting(filterContext); } } |
这个过滤器适合做系统操作日志记录功能:
1
2
3
4
5
6
|
[SystemLog(Operate = "添加用户" )] public string CustomerFilterTest() { Response.Write( "<br/>Action 执行中..." ); return "<br/>Action 执行结束" ; } |
看下结果:
四个方法执行顺序:OnActionExecuting—>OnActionExecuted—>OnResultExecuting—>OnResultExecuted,非常精确的控制了整个请求过程。
实战中记录日志过程是这样的:在OnActionExecuting方法里写一条操作日志到数据库里,全局变量存下这条记录的主键,到OnResultExecuted方法里说明请求结束了,这个时候自然知道用户的这个操作是否成功了,根据主键更新下这条操作日志的是否成功字段。
二、模型绑定(ModelBinder)
先看一个普通的方法:
1
2
3
4
|
public ActionResult Index(Student student) { return View(); } |
这个方法接受的参数是一个Student对象,前端传递过来的参数跟Student对象里的属性保持一直,那么就自动被绑定到这个对象里了,不需要在方法里new Student这个对象并挨个绑定属性了,绑定的过程由MVC中的DefaultModelBinder完成的,DefaultModelBinder同时继承了IModelBinder接口,现在就利用IModelBinder接口和DefaultModelBinder来实现更加灵活的模型绑定。
场景一、前端传过来了一个加密的字符串token,方法里需要用token里的某些字段,那就得在方法里接收这个字符串、解密字符串、转换成对象,这样一个方法还好说,多了的话重复代码非常多,就算提取通用方法,还是要在方法里调用这个通用方法,有没有办法直接在参数里就封装好这个对象?
模型绑定的对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class TokenModel { /// <summary> /// 主键 /// </summary> public int Id { get ; set ; } /// <summary> /// 姓名 /// </summary> public string Name { set ; get ; } /// <summary> /// 简介 /// </summary> public string Description { get ; set ; } } |
新建一个TokenBinder继承IModelBinder接口并实现其中的BindModel方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public class TokenBinder : IModelBinder { public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { var token = controllerContext.HttpContext.Request[ "token" ]; if (! string .IsNullOrEmpty(token)) { string [] array = token.Split( ':' ); if (array.Length == 3) { return new TokenModel() { Id = int .Parse(array[0]), Name = array[1], Description = array[2] }; } else { return new TokenModel() { Id = 0 }; } } else { return new TokenModel() { Id = 0 }; } } } |
这个方法里接收了一个token参数,并对token参数进行了解析和封装。代码部分完成了需要到Application_Start方法里进行下注册:
1
|
ModelBinders.Binders.Add( typeof (TokenModel), new TokenBinder()); |
现在模拟下这个接口:
1
2
3
4
5
|
public JsonResult TokenBinderTest(TokenModel tokenModel) { var output = "Id:" + tokenModel.Id + ",Name:" + tokenModel.Name + ",Description:" + tokenModel.Description; return Json( new ReturnModel_Common { msg = output }); } |
调用下:
可以看出,“1:汪杰:oppoic.cnblogs.com”已经被绑定到tokenModel这个对象里面了。但是如果稍复杂的模型绑定IModelBinder就无能为力了。
场景二、去除对象某个属性的首位空格
1
2
3
4
5
6
7
8
|
public class Student { public int Id { get ; set ; } public string Name { get ; set ; } public string Class { get ; set ; } } |
如果前端传来的Name属性有空格,如何去除呢?利用DefaultModelBinder即可实现更灵活的控制
1
2
3
4
5
6
7
8
9
10
11
12
|
public class TrimModelBinder : DefaultModelBinder { protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder) { var obj = base .GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder); if (obj is string && propertyDescriptor.Attributes[ typeof (TrimAttribute)] != null ) //判断是string类型且有[Trim]标记 { return (obj as string ).Trim(); } return obj; } } |
标注下需要格式化首位属性的实体:
1
2
3
4
5
6
7
8
9
10
|
[ModelBinder( typeof (TrimModelBinder))] public class Student { public int Id { get ; set ; } [Trim] public string Name { get ; set ; } public string Class { get ; set ; } } |
好了,测试下:
1
2
3
4
5
6
7
8
9
10
11
|
public JsonResult TrimBinderTest(Student student) { if ( string .IsNullOrEmpty(student.Name) || string .IsNullOrEmpty(student.Class)) { return Json( new ReturnModel_Common { msg = "未找到参数" }); } else { return Json( new ReturnModel_Common { msg = "Name:" + student.Name + ",长度:" + student.Name.Length + " Class:" + student.Class + ",长度:" + student.Class.Length }); } } |
可见,标注了Trim属性的Name长度是去除空格的长度:7,而没有标注的Class属性的长度则是6。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:http://www.cnblogs.com/oppoic/p/6407896.html?utm_source=tuicool&utm_medium=referral