第九节:从源码的角度分析MVC中的一些特性及其用法
一. 前世今生
乍眼一看,该标题写的有点煽情,最近也是在不断反思,怎么能把博客写好,让人能读下去,通俗易懂,深入浅出。
接下来几个章节都是围绕框架本身提供特性展开,有MVC程序集提供的,也有其它程序集提供;在本章节将重点介绍几个MVC框架提供的且作用于方法上的特性,并且模仿其源码自定义特性。
其实早在前面的 DotNet进阶章节,就写过一篇关于特性的文章了,这里重新总结一下特性核心要点。
1. 什么是特性?
特性是一个类,在不影响程序封装的情况下,额外的给程序添加一些信息,用于运行时描述程序或者影响程序的行为。
2. 特性的作用范围?
提到特性的作用范围,就不得不提到 AttributeUsage了,该类本身就是一个特性,继承了Attribute类,用于约束自定义特性(你可以看到系统提供的很多特性中,均能看到它的身影),下面先看一 下它的源码:
该特性有一个参数,两个核心属性,AttributeTargets参数约束了该给特性可以作用的范围,通过右面的代码可知,可以作用于:类、方法、参数、属性、返回值等等,该参数默认为ALL。
AllowMultiple:约束该特性能否同时作用于某个元素(类、方法、属性等等)多次,默认为false。
Inherited:约束该特性作用于基类(或其它)上,其子类能否继承该特性,默认为true。
3. 如何自定义特性?
简单来说,声明一个以Attribute结尾的类,继承Attribute类,然后加上AttributeTargets特性约束,一个简单的特性就产生了。
二. MVC中的常用特性
有了前面的铺垫,这里讲解【System.Web.Mvc】程序集下的一些特性就很好理解,理解源码的同时,主要掌握其如何使用。
MVC中提供的常用特性有:【HttpGet】、【HttpPost】、【AcceptVerbs】、【ActionName】、【NoAction】、【AllowAnonymous】、【ValidateAntiForgeryToken】、【ChildActionOnly】、【Bind】这九个特性。
查看源码可知,其中【HttpGet】【HttpPost】【AcceptVerbs】【ActionName】【NoAction】【ChildActionOnly】均继承ActionNameSelectorAttribute类,实现了IsValidForRequest这个抽象方法,且特性约束为: [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)],显然这些特性均是作用于方法上的,且不需要同时作用,允许子类继承该特性。
以【HttpGet】的源码为例:
其中【AllowAnonymous】直接继承Attribute类,源码如下:
其中【ValidateAntiForgeryToken】继承了FilterAttribute类,实现了IAuthorizationFilter接口,源码如下:
(扩展一下:AuthorizeAttribute类也是继承了FilterAttribute类,实现了IAuthorizationFilter接口)
1. HttpGet和HttpPost
(1). HttpGet:只允许Get请求访问
底层运用的AcceptVerb特性实现的,所以等价于[AcceptVerbs(HttpVerbs.Get)]或[AcceptVerbs("Get")]
测试:前端用Ajax请求,如果非Get请求方式进行请求,则提示404找不到
(2). HttpPost:只允许Post请求访问
底层运用的AcceptVerb特性实现的,所以等价于[AcceptVerbs(HttpVerbs.Post)]或[AcceptVerbs("Post")]
测试:前端用Ajax请求,如果非Post请求方式进行请求,则提示404找不到
特别注意:如果一个方法要同时允许Get和Post请求[HttpGet]和[HttpPost]同时加载上面是错误的!!这个时候就需要使用AcceptVerb特性了(当然方法上如果什么特性也不加,什么请求均支持)
2. AcceptVerbs
AccetpVerbs:用于限定请求方式(包括:Get、Post、Put、Delete、Head、Patch、Options)
查看源码可知:该特性有两个构造函数,所有两种写法,如:只允许Get请求,可以:[AcceptVerbs(HttpVerbs.Get)]和[AcceptVerbs("Post")]
若要同时支持多种请求,可以:[AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post)]或[AcceptVerbs("Get", "Post")]
相关测试代码如下:
1 //1. 下面三种写法均为只允许Get请求 2 //[HttpGet] 3 //[AcceptVerbs(HttpVerbs.Get)] 4 //[AcceptVerbs("Get")] 5 6 //2. 下面三种写法均为只允许Get请求 7 //[HttpPost] 8 //[AcceptVerbs(HttpVerbs.Post)] 9 //[AcceptVerbs("Post")] 10 11 //3. 下面两种写法表示:既允许Get请求也允许Post请求 12 [AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post)] 13 //[AcceptVerbs("Get", "Post")] 14 public ActionResult TestMethordWay() 15 { 16 return Content("请求成功"); 17 }
1 //1. 测试只允许Get或Post请求 2 $("#btn1").click(function () { 3 $.ajax({ 4 type: "Put", //Post 、Put 5 url: "TestMethordWay", 6 data: "", 7 success: function (msg) { 8 alert(msg); 9 } 10 }); 11 });
3. ActionName
ActionName:修改Action本身的方法名
测试:请求TestActionName1方法,报404找不到
请求TestActionName2方法,正常访问
相关测试代码如下:
1 /// <summary> 2 /// 名字变为:TestActionName2了 3 /// </summary> 4 /// <returns></returns> 5 [ActionName("TestActionName2")] 6 public ActionResult TestActionName1() 7 { 8 return Content("我是TestActionName1"); 9 }
1 //2. 测试ActionName特性 2 $("#btn2").click(function () { 3 $.ajax({ 4 type: "Get", 5 url: "TestActionName2", //TestActionName1 6 data: "", 7 success: function (msg) { 8 alert(msg); 9 } 10 }); 11 });
4. NoAction
NoAction: 标记控制器中的action将不在是一个方法,不能前端Http请求访问
测试:前端页面ajax进行请求,报404找不到
但在其它action中进行调用,能正常调用
相关测试代码如下:
1 /// <summary> 2 /// 标记该方法将不是一个方法 3 /// </summary> 4 /// <returns></returns> 5 [NonAction] 6 public ActionResult TestNoAction() 7 { 8 return Content("请求成功"); 9 }
1 //3. 测试NoAction特性 2 $("#btn3").click(function () { 3 $.ajax({ 4 type: "Get", 5 url: "TestNoAction", //TestNoAction 6 data: "", 7 success: function (msg) { 8 alert(msg); 9 } 10 }); 11 });
5. AllowAnonymous
AllowAnonymous:该特性用于标记在授权期间要跳过 AuthorizeAttribute 过滤器的验证
解释:AuthorizeAttribute是MVC框架自带的实现IAuthorizationFilter过滤器的一个类,内部有一套自身业务验证(感兴趣的可以自己研究源码)
而AllowAnonymous就是为了标记跨过AuthorizeAttribute验证的
这里不做详细测试
6. ValidateAntiForgeryToken
ValidateAntiForgeryToken:阻止跨站请求伪造攻击(CSRF).
①. CSRF原理是什么:
a.用户mr访问正规网站A,登录验证通过,并在用户mr处产生Cookie
b.用户mr在不关闭A网站的情况下打开危险的B网站,在B网站中要求访问A网站,发出一个Request请求
c.这时候浏览器带着A网站在mr出产生的Cookie进行访问A网站
d.这时候A网站就无法判断这个cookie是谁产生的,默认就给通过了
详细见:https://www.cnblogs.com/hechunming/p/4647646.html
②:解决方案
a. 在Controller中的action上加上特性[ValidateAntiForgeryToken]
b. 对于增删改查操作前端调用: $.ajaxAntiForgery方法进行ajax请求(需要引入jqueryToken的js文件)
这里不做详细测试
7. ChildActionOnly
ChildActionOnly:限制操作方法只能由子操作进行调用。
①. 测试直接输入:http://localhost:7559/SpecialAttribute/Index2, 无法访问报错.
②. 需要通过RenderAction来调用(存在问题,与Unity改造框架冲突冲突)
这里RenderAction不做详细测试
8. Bind
①. 源码的角度分析:该特性可以作用于类或参数(本章节测试作用于参数)
②. 该特性有三个核心属性:
a. Exclude:获取或设置不允许绑定的属性名称的列表(各属性名称之间用逗号分隔)
b. Include:获取或设置允许绑定的属性名称的列表(各属性名称之间用逗号分隔),与Exclude一个道理,通常根据情况使用一个即可
c. Prefix:获取或设置在呈现表示绑定到操作参数或模型属性的标记时要使用的前缀
不适用与ajax提交,适用于razor语法中的@{Html.TextBox(stu.id)},在现在前后端分离盛行的情况下,有点不适合了
测试:
前端过个ajax传过来三个参数:id、name、sex, 参数中的stu只能收到id和name,sex为null,若想收到sex,需要在方法中通过request进行接收
1 /// <summary> 2 /// stu中的sex属性为null 3 /// var 中sex为男 4 /// </summary> 5 /// <param name="stu"></param> 6 /// <returns></returns> 7 public ActionResult TestBindAttribute([Bind(Include = "id,name")]Student stu) 8 { 9 var sex = Request["sex"]; 10 return Content("请求成功"); 11 } 12
1 //4.测试BindAttribute特性 2 $("#btn4").click(function () { 3 $.ajax({ 4 type: "Get", 5 url: "TestBindAttribute", 6 data: {"id":"123","name":"ypf","sex":"男"}, 7 success: function (msg) { 8 alert(msg); 9 } 10 }); 11 });
三. 自定义一个类似的特性
要求:支持Get请求,且必须是Ajax请求
思路:由HttpGet特性可以知道:需要继承ActionMethodSelectorAttribute类,然后覆写IsValidForRequest方法即可
1 public class HttpGetAndAjaxAttribute : ActionMethodSelectorAttribute 2 { 3 public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) 4 { 5 //1.获取请求方式 6 string HttpWay = controllerContext.HttpContext.Request.GetHttpMethodOverride(); 7 8 //2. 获取是否是Ajax请求 9 bool isAjax = controllerContext.HttpContext.Request.IsAjaxRequest(); 10 11 if (HttpWay.ToLower() == "get" && isAjax == true) 12 { 13 return true; 14 } 15 return false; 16 17 } 18 } 19 20 [HttpGetAndAjax] 21 public ActionResult GetAndAjax() 22 { 23 return Content("请求成功"); 24 }
1 //5.测试自定义特性 2 $("#btn5").click(function () { 3 $.ajax({ 4 type: "Get", 5 url: "GetAndAjax", 6 data: "", 7 success: function (msg) { 8 alert(msg); 9 } 10 }); 11 });