第十五节:HttpContext五大核心对象的使用(Request、Response、Application、Server、Session) 第十四节:再探MVC中路由的奥秘 第十三节:HttpHander扩展及应用(自定义扩展名、图片防盗链) 第十二节:MVC中的一些特殊优化 第十一节:Bundles压缩合并js和css及原理分析 第十节:数据批注(DataAnnotationMode
第十五节:HttpContext五大核心对象的使用(Request、Response、Application、Server、Session)
一. 基本认识
1. 简介:HttpContext用于保持单个用户、单个请求的数据,并且数据只在该请求期间保持;
也可以用于保持需要在不同的HttpModules和HttpHandlers之间传递的值;
也可以用于保持某个完整请求的相应信息。
2. 五大核心对象包括:Response、Request、Application、Server、Session
3. 源码分析:在MVC框架中HttpContext对象来源于一个HttpContextBase类的一个实例,该类中包括以下几个重要属性:(非MVC框架中有一个单独HttpContext类)
①:HttpRequestBase Request
②:HttpResponseBase Response
③:HttpApplicationStateBase Application
④:HttpServerUtilityBase Server
⑤:HttpSessionStateBase Session
即所谓的五大核心对象,所以在MVC框架中通常可以这么用: HttpContext.Request、HttpContext.Response、HttpContext.Application、 HttpContext.Server、HttpContext.Session .
但我们也经常看到省略HttpContext对象,直接:Request、Response、Server、Session 来使用,随意选中其中一个,点击F12查看源码可知,Controller类中也包含以下四个属性:
①:HttpRequestBase Request
②:HttpResponseBase Response
③:HttpServerUtilityBase Server
④:HttpSessionStateBase Session
二. 逐个分析
1. Request对象
1. Request:接收处理客户端发送过来的请求 (已完成)
①:HttpMethod属性:获取客户端发送请求的类型
②:RawUrl属性:获取当前请求完整的URL
③:UrlReferrer属性:获取有关链接到当前 URL 的客户端请求的 URL
④:Request["XX"]:获取xx属性值
2. 代码测试
1 //1.测试Request对象 2 $("#btn1").click(function () { 3 $.ajax({ 4 type: "Post", 5 url: "TestRequest", 6 data: { 7 "name": "123" 8 }, 9 success: function (data) { 10 if (data.status == "ok") { 11 alert("测试通过"); 12 $("#s1").html(data.HttpMethod); 13 $("#s2").html(data.RawUrl); 14 $("#s3").html(data.UrlReferrer); 15 $("#s4").html(data.name); 16 } 17 if (data == "error") { 18 alert("测试未通过"); 19 } 20 } 21 }); 22 });
1 /// <summary> 2 /// 测试Request对象相关 3 /// </summary> 4 /// <returns></returns> 5 public ActionResult TestRequest() 6 { 7 var HttpMethod = Request.HttpMethod; 8 var RawUrl = Request.RawUrl; 9 var UrlReferrer = Request.UrlReferrer; 10 var name = Request["name"]; 11 var data = new 12 { 13 status="ok", 14 HttpMethod=HttpMethod, 15 RawUrl= RawUrl, 16 UrlReferrer= UrlReferrer, 17 name= name 18 }; 19 return Json(data); 20 }
测试结果:
2. Server对象
1. Server:一个辅助类 (已完成)
①.HtmlEncode方法:将html编码转换成对应的字符串
②.HtmlDecode方法:将字符串转换成Html编码
③.MapPath方法:获取该地址对应的物理路径
④.UrlEncode方法:将url编码转换成对应的字符串
⑤.UrlDecode方法:将对应的字符串转换成Url格式的编码
2. 代码测试
1 //2.测试Server对象 2 $("#btn2").click(function () { 3 $.ajax({ 4 type: "Post", 5 url: "TestServer", 6 data: { 7 8 }, 9 success: function (data) { 10 if (data.status == "ok") { 11 alert("测试通过"); 12 $("#s11").html(data.HtmlEncode); 13 $("#s22").html(data.HtmlDecode); 14 $("#s33").html(data.MapPath); 15 $("#s44").html(data.UrlEncode); 16 $("#s55").html(data.UrlDecode); 17 } 18 if (data == "error") { 19 alert("测试未通过"); 20 } 21 } 22 }); 23 });
1 public ActionResult TestServer() 2 { 3 string HtmlEncode = Server.HtmlEncode("<p>积极向上</p>"); 4 string HtmlDecode = Server.HtmlDecode(HtmlEncode); 5 string MapPath = Server.MapPath("/home/index"); //只能做物理文件的映射 6 string UrlEncode = Server.UrlEncode("https://www.2345.com/"); 7 string UrlDecode = Server.UrlDecode(UrlEncode); 8 var data = new 9 { 10 status = "ok", 11 HtmlEncode = HtmlEncode, 12 HtmlDecode = HtmlDecode, 13 MapPath = MapPath, 14 UrlEncode = UrlEncode, 15 UrlDecode = UrlDecode 16 }; 17 return Json(data); 18 }
测试结果:
3. Application对象
1. Application: 用于在ASP.NET 应用程序内的多个会话和请求之间共享信息(案例:单点登录)
①. Lock方法:锁定对象的访问
②. unLock方法:取消锁定对象的访问
③. Add和Set方法:向集合中添加一个新对象
④. Application[]和Get方法:获取对象的值
⑤. Remove、RemoveAt、RemoveAll方法:用来删除对象
补充一个单一对话: HttpContextBase context.Item[""] ; 是指是一个IDictionary类,有Add、Clear、Contains、Remove等方法(不做测试)
2. 测试:
①. 用不同的浏览器打开Index页面,模拟多线程多个会话,发现name2的值是相同的。
4. Response对象
1. Response:用户服务器端将信息返回给客户端
①.ContentType属性:获取或设置当前响应的 HTTP MIME 类型 (非常重要!),下面补充几个常用的类型(多用于下载)
a. Json格式: "application/json"
b. JavaScript格式:"application/x-javascript"
c. 普通文本格式:"text/plain"
d. Html格式:"text/html"
e. XML格式:"text/xml"
f. Excel格式:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
g. gif图片格式:"image/gif"
②.Redirect方法:用于站内跳转或站点间的跳转
③.Write方法:可以将字符、字符串、字节等写入到Http响应输出流 (MVC中的Content方法就是基于该方法进行扩展的)
④.WriteFile和TransmitFile方法:将指定文件写入到Http相应输出流 (MVC中的FileResult就是基于该方法封装的,简化了下载流程)
(原生的四种下载方法详见:https://www.cnblogs.com/weixing/archive/2012/02/27/2369567.html (不做测试了))
2. 测试利用FileResult下载文件
1 /// <summary> 2 /// 测试下载文件 3 /// </summary> 4 /// <param name="type">1代表图片 2代表Excel文件</param> 5 /// <returns></returns> 6 public ActionResult TestDownFile(string type) 7 { 8 if (type == "1") 9 { 10 //需要下载的文件在服务器上的物理路径 11 string path = Server.MapPath("/Content/imageHandle/pipe1.jpg"); 12 //需要下载的文件下载后保存到本地后的名字 13 string fileName = DateTime.Now.ToString("yyyyMMddHHmmssffffff") + ".jpg"; 14 return File(path, "image/jpg", fileName); 15 } 16 if (type == "2") 17 { 18 string path = Server.MapPath("/Content/myExcel/text.xlsx"); 19 string fileName = DateTime.Now.ToString("yyyyMMddHHmmssffffff") + ".xlsx"; 20 return File(path, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName); 21 } 22 return Content(""); 23 } 24
1 //4. 测试下载文件 2 //4.1 下载图片 3 $("#btn5").click(function () { 4 window.location.href = "TestDownFile?type=1"; 5 }); 6 //4.2 下载Excel 7 $("#btn6").click(function () { 8 window.location.href = "TestDownFile?type=2"; 9 });
结果:
5. Session对象
Session:存储于服务器内存中key-value集合
(1).补充Cookie的概念:Cookie存储于客户端,通过浏览器的【Response Header】标头下的Set-Cookie进行存储,由浏览器进行建立、
保存、和到期删除。Cookie默认的生命周期是浏览器的声明周期,浏览器关闭,cookie消失,当然也可以显示的设置Cookie的到期时间,浏览器会根据这个到期
时间,将Cookie存放到客户端硬盘的。
注意:Cookie在浏览器可以被看到,所以不适合存放敏感信息;Cookie能存储的数据有限,最大4kb;Cookie是按照浏览器进行划分的。
(2).Http的请求流程:
①:客户端第一次访问,没有Cookie,没有Session,服务器端会生成一个SessionId和一个空的Value,保存到服务器内存上。
②:客户端第一次返回,通过浏览器的【Response Header】标头下的Set-Cookie进行输出。
③:客户端第二次访问,会带上Cookie进行访问,且Cookie中存放着SessionId,服务器端通过SessionMoudle解析得到SessionId,获取内存
中存放的数据,并且知道刚才你来过。
④:接下来在Session没有过期和不关闭的浏览器的情况下,每次访问服务器端,都会带着同一个SessionId进行访问。
图解:
(3).深入剖析Session
①:同一个浏览器在不关闭的情况下(且服务器端Session没有过期),无论访问几次服务器端,SessionId都是相同的,因为每次请求都会带着Cookie中存储的SessionId的。
②:关闭浏览器,再次请求服务器,因为【Request Header】表头中,没有提交刚才的SessionId,服务器会认为这是一个新的请求,服务器会分配给你一个新的
SessionId,这就解释了原SessionId并没有过期为什么服务器会重新分配给你一个SessionId的原因了。
③:同一个浏览器登录状态下,长时间没有任何操作(Session默认过期时间为20分钟),再次操作的时候,虽然Cookie中带着SessionId进行
访问服务器端,但是原Session已经被服务器端给清除了,只保留SessionId,原存放到Session中的内容均为null。
④:Session不能跨进程访问,只能由当前会话的用户访问,因为SessionId是以Cookie的形式存放在访问者的客户端浏览器中。
⑤:在同一个会话中,Session是可以跨页面进行全局使用的。
⑥:服务器上的Session保存的是一个SessionId和一个Value,而Value是一个集合,该集合中存放着当前用户的Key
(4).日常开发Session的局限性
在我们开发管理系统中,通常登录后会把该用户的权限放到Session中(如:放到Session["user"]中),这时候,在同一个浏览器中登录admin账户,
会把a、b、c权限放到Session["user"]中,在不关闭该浏览器的情况下,登录admin2账户,这时候同一个浏览器请求的【Request Header】表头中
的cookie存放的SessionId是相同的,所以服务器端会认为是同一个会话,admin2账户的c,d权限会把原先的Session["user"]中的a、b、c权限覆盖,
导致admin账户操作系统的时候,发现自己没有 a、b、c权限,而是有c、d权限。
(如果解决这个问题呢?可以使用NoSQL来替代Session)
(5).Session的声明和销毁
①:从写入开始,如果该页面一直没有操作,默认20分钟,服务器会把session销毁,但SessionId还是存在的。
②:手动通过Abandon的方式取消当前会话
③:删除客户端的Cookie,会导致服务器端重新分配给客户端一个SessionId,其实服务器端的原Session并没有消失
(6).总结:
客户端向服务器发请求,如果请求头中没有带SessionId,服务器会分配一个SessionId给客户端,并存放在客户端的Cookie中;
客户端向服务器发送请求,如果cookie中有值,会带着该值一起发送到服务器,如果没有,则不带。
(7).补充Session的几个常用方法
①:Session[]:用于设置或读取Session的值
②:Timeout属性:设置Session过期时间
(补充通过配置文件的形式设置全局session过期:System.Web下添加 <sessionState mode="InProc" timeout="30"/> )
InProc表示进程内;timeout表示多少分钟后Session失效。
③:SessionID属性:每个会话的唯一标识符
④:Remove方法:从会话集合中删除一项;Clear方法:从会话集合中删除所有的键和值;Abandon方法:取消当前会话
MVC中的TempData就是基于Session实现的,但是TempData只能读取一次
(8). 测试:
①:测试同一个浏览器,不关闭情况下SessionId相同,且同一个key的数据会被后面覆盖
②:测试服务器端Session过期后,SessionId还存在,Session中的值均为Null(销毁也是这个效果)
③:通过F12 浏览器验证http流程,并查看【Response Header】和【Request Header】中什么时候又SessionId
④:手动删除客户端的Cookie,再次请求服务器,会重新分配新的SessionId
先贴出来几块代码:
a:打开该页面,显示SessionId和name的值,如下图:
b:分析该页面打开的请求,如下图:印证Http请求第一次请求和第一次返回的请求流程
c. 点击按钮多次,获取SessionId和name的值,结果如下图:SessionId和name的值都是相同的。
d. 查看该按钮对应的请求,如下图:
e: 不关闭该浏览器的情况下,重新在一个新的选项卡中打开Index页面,SessionId没有变,name重新赋值了。
f:回到第一次打开的页面,重新点击测试按钮,获取SessionId和name值,SessionId没有变,但是name值被第二次打开的页面的name值覆盖了。(印证了管理系统中权限覆盖的问题)
g:点击销毁Session,然后重新点击测试按钮,查看结果,SessionId依旧存在,但是Session的value集合变成null了(等待服务器session自动过期也是这种效果)
第十四节:再探MVC中路由的奥秘
一. 基于RouteBase扩展
1. 原理
扩展RouteBase,同样使用的是MVC框架提供的MvcRouteHandler进行处理。
2. 步骤
1. 新建YpfRoute1类,继承RouteBase类型,并覆写里面的两个抽象方法, 在GetRouteData编写业务逻辑,(这里是禁止Chrome浏览器访问),通过RouteData添加地址,并返回。
1 /// <summary> 2 /// 基于RouteBase进行扩展 3 /// </summary> 4 public class YpfRoute1 : RouteBase 5 { 6 /// <summary> 7 /// 解析路由信息 8 /// </summary> 9 /// <param name="httpContext"></param> 10 /// <returns></returns> 11 public override RouteData GetRouteData(HttpContextBase httpContext) 12 { 13 //编写自己业务逻辑(这里禁止Chrome浏览器的访问 不是很准确) 14 if (httpContext.Request.UserAgent.IndexOf("Chrome")>=0) 15 { 16 //返回指定的页面 17 RouteData rd = new RouteData(this, new MvcRouteHandler()); 18 rd.Values.Add("controller","Route"); 19 rd.Values.Add("action", "RefuseView"); 20 return rd; 21 } 22 23 return null; 24 } 25 26 /// <summary> 27 /// 指定虚拟路径处理(这里暂不使用) 28 /// </summary> 29 /// <param name="requestContext"></param> 30 /// <param name="values"></param> 31 /// <returns></returns> 32 public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) 33 { 34 throw new NotImplementedException(); 35 } 36 }
2. 在RouteConfig文件中新建RefuseChrome方法,并在该方法中添加YpfRoute1类。
1 /// <summary> 2 /// 基于RouteBase进行扩展的路由 3 /// </summary> 4 /// <param name="route"></param> 5 public static void RefuseChrome(RouteCollection route) 6 { 7 route.Add("myRoute", new YpfRoute1()); 8 }
3. 在Global中将RefuseChrome方法注册进去。
注意:是放到系统自带路由注册的上面还是下面,哪个在上,哪个先执行。
4. 测试
使用Chrome浏览器打开任何页面,均跳转到拒绝访问页面。
二. 基于IRouteHandler扩展
1. 原理
扩展IRouteHandler并且自定义IHttpHandler 。
2. 步骤
1. 新建YpfRoute1类,实现接口IRouteHandler,并实现里面的GetHttpHandler方法。
2. 自定义ypfHttpHandler类,并实现IhttpHandler接口。
1 /// <summary> 2 /// 扩展IRouteHandler和自定义IHttpHandler 3 /// </summary> 4 public class YpfRoute2 : IRouteHandler 5 { 6 public IHttpHandler GetHttpHandler(RequestContext requestContext) 7 { 8 return new ypfHttpHandler(); 9 } 10 } 11 12 public class ypfHttpHandler : IHttpHandler 13 { 14 15 bool IHttpHandler.IsReusable 16 { 17 get 18 { 19 return false; 20 } 21 } 22 23 void IHttpHandler.ProcessRequest(HttpContext context) 24 { 25 string url = context.Request.Url.AbsoluteUri; 26 context.Response.Write(string.Format("这里是ypf定制:{0}", this.GetType().Name)); 27 context.Response.Write((string.Format("当前地址为:{0}", url))); 28 29 context.Response.End(); 30 } 31 }
3. 在RouteConfig文件中新建RegisterSpecialRoute方法,并在该方法中添加YpfRoute1类(注册规则,并使用RouteTable往里添加)
1 /// <summary> 2 /// 扩展IRouteHandler和自定义IHttpHandler 3 /// </summary> 4 /// <param name="routes"></param> 5 public static void RegisterSpecialRoute(RouteCollection routes) 6 { 7 Route route = new Route("Ypf/{other}",new YpfRoute2()); 8 RouteTable.Routes.Add(route); 9 }
4. 在Global中将RefuseChrome方法注册进去,注意:是放到系统自带路由注册的上面还是下面,哪个在上, 哪个先执行。
5. 访问地址:http://localhost:7559/Ypf/1 ,统一跳转到指定页面显示指定的信息(地址中的 1 可以换成任何信息)
第十三节:HttpHander扩展及应用(自定义扩展名、图片防盗链)
一. 自定义扩展名
1. 前言
凡是实现了IHttpHandler接口的类均为Handler类,HttpHandler是一个HTTP请求的真正处理中心,在HttpHandler容器中,ASP.NET Framework才调用HttpHandler的ProcessRequest方法来对这个HTTP请求进行真正的处理,真正地对客户端请求的服务器页面做出编译和执行,并将处理过后的信息附加在HTTP请求信息流中再次返回到HttpModule中。
2. 背景
我们在日常开发中,也许会碰到一些这样的特殊需求,在路由规则之外,想给自己预留一个后门,进行一些小动作;或者想自定义一种特有的看起来很酷炫的后缀;或者默写后缀不想走MVC框架默认提供的处理流程,那么使用HttpHander进行扩展,再合适不过。
下面我们先扩展一个后缀为 .ypf的处理请求,凡是该后缀的请求,统一显示验证码页面。
3. 详细步骤
①. 新建一个类(ypfCode),实现IHttpHandler接口。
1 /// <summary> 2 ///这里自定义的handle为一般处理程序,实现了IHttpHandler接口 3 ///IRequiresSessionState是为了使http请求具有会话状态的读写权限,即能操控: context.Session["CheckCode"] = code; 4 /// </summary> 5 public class ypfCode : IHttpHandler, IRequiresSessionState 6 { 7 /// <summary> 8 /// 您将需要在网站的 Web.config 文件中配置此处理程序 9 /// 并向 IIS 注册它,然后才能使用它。有关详细信息, 10 /// 请参阅以下链接: https://go.microsoft.com/?linkid=8101007 11 /// </summary> 12 #region IHttpHandler Members 13 14 public bool IsReusable 15 { 16 // 如果无法为其他请求重用托管处理程序,则返回 false。 17 // 如果按请求保留某些状态信息,则通常这将为 false。 18 get { return true; } 19 } 20 21 public void ProcessRequest(HttpContext context) 22 { 23 //在此处写入您的处理程序实现。 24 string code = ""; 25 Bitmap bitmap = VerifyCodeHelper.CreateVerifyCode(out code); 26 context.Session["CheckCode"] = code; 27 bitmap.Save(context.Response.OutputStream, ImageFormat.Gif); 28 context.Response.ContentType = "image/gif"; 29 } 30 #endregion 31 }
补充一个验证码实现类:
1 public class VerifyCodeHelper 2 { 3 public static Bitmap CreateVerifyCode(out string code) 4 { 5 //建立Bitmap对象,绘图 6 Bitmap bitmap = new Bitmap(200, 60); 7 Graphics graph = Graphics.FromImage(bitmap); 8 graph.FillRectangle(new SolidBrush(Color.White), 0, 0, 200, 60); 9 Font font = new Font(FontFamily.GenericSerif, 48, FontStyle.Bold, GraphicsUnit.Pixel); 10 Random r = new Random(); 11 string letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ0123456789"; 12 13 StringBuilder sb = new StringBuilder(); 14 15 //添加随机的五个字母 16 for (int x = 0; x < 5; x++) 17 { 18 string letter = letters.Substring(r.Next(0, letters.Length - 1), 1); 19 sb.Append(letter); 20 graph.DrawString(letter, font, new SolidBrush(Color.Black), x * 38, r.Next(0, 15)); 21 } 22 code = sb.ToString(); 23 24 //混淆背景 25 Pen linePen = new Pen(new SolidBrush(Color.Black), 2); 26 for (int x = 0; x < 6; x++) 27 graph.DrawLine(linePen, new Point(r.Next(0, 199), r.Next(0, 59)), new Point(r.Next(0, 199), r.Next(0, 59))); 28 return bitmap; 29 } 30 }
②. 在Web.config文件中的 <system.webServer>→<handlers>下添加以下节点,表示以.ypf为后缀的请求统一由ypfCode处理,其中
<add name="ypf" path="*.ypf" verb="*" type="Ypf.Web.Core.PipeLine.ypfCode,Ypf.Web.Core" />
Ypf.Web.Core.PipeLine.ypfCode:表示该类的命名空间
Ypf.Web.Core:表示该类所在库的程序集名称
1 <!--VS2013及以后/IIS7.0之后的集成模式 需要添加下面的system.webServer节点--> 2 <system.webServer> 3 4 <!--1. 在此处配置modules--> 5 <modules runAllManagedModulesForAllRequests="false"> 6 <!--1.1 runAllManagedModulesForAllRequests处理静态文件的请求--> 7 <remove name="FormsAuthentication" /> 8 <!--1.2 优化网站性能,去掉不需要的module--> 9 <remove name="RoleManager" /> 10 <remove name="FileAuthorization" /> 11 <remove name="UrlAuthorization" /> 12 </modules> 13 <!--2. 在此处配置handlers--> 14 <handlers> 15 <!--2.1 优化网站性能,去掉不需要的module--> 16 <remove name="ExtensionlessUrlHandler-Integrated-4.0" /> 17 <remove name="OPTIONSVerbHandler" /> 18 <remove name="TRACEVerbHandler" /> 19 <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" /> 20 21 <!--2.2 添加自定义handlers--> 22 <!--2.2.1 后缀为ypf的请求 --> 23 <add name="ypf" path="*.ypf" verb="*" type="Ypf.Web.Core.PipeLine.ypfCode,Ypf.Web.Core" /> 24 25 <!--2.2.2 图片处理请求 --> 26 <!--方案一 逐个添加不同格式的图片 特别注意:不支持在一个path写多个扩展名--> 27 <!--<add name="img" path="*.jpg" verb="*" type="Ypf.Web.Core.PipeLine.imgHandle,Ypf.Web.Core" /> 28 <add name="png" path="*.png" verb="*" type="Ypf.Web.Core.PipeLine.imgHandle,Ypf.Web.Core" /> 29 <add name="gif" path="*.gif" verb="*" type="Ypf.Web.Core.PipeLine.imgHandle,Ypf.Web.Core" />--> 30 <!--方案二 通过HangerFactory来处理 特别注意:不支持在一个path写多个扩展名 --> 31 <add name="img" path="*.jpg" verb="*" type="Ypf.Web.Core.PipeLine.ImageHandlerFactory,Ypf.Web.Core" /> 32 <add name="png" path="*.png" verb="*" type="Ypf.Web.Core.PipeLine.ImageHandlerFactory,Ypf.Web.Core" /> 33 <add name="gif" path="*.gif" verb="*" type="Ypf.Web.Core.PipeLine.ImageHandlerFactory,Ypf.Web.Core" /> 34 35 </handlers> 36 </system.webServer>
③. 由于本身框架本身已经有一套路由规则用来处理http请求,所以需要在RouteConfig中忽略该自定义Handle的规则,如下:
routes.IgnoreRoute("MyTest/{*pathInfo}"); 表示MyTest/格式的请求不进行路由验证
④. 测试:http://localhost:7559/MyTest/xxx.ypf 均返回验证码图片
二. 图片防盗链
1. 什么是图片防盗链
自己网站中展示的自己的图片通常是放在自己服务器上,很多无耻的网站获取到别人网站的图片地址,放到自己网站上,这就是图片盗链。
图片被盗链损耗的是自己服务器的流量。
2. 防盗链的代码实现原理
当接受到一张图片请求的时候
1. 判断该图片请求是从哪一个请求连接过来的(或者从哪一个页面连接过来的),如果为空,表示是一个单独的图片请求,肯定不该站内的,
所以判断为盗链。两种判断方式如下
a: context.Request.ServerVariables["HTTP_REFERER"]
b: context.Request.UrlReferrer
2. 如果上一个请求不为空,则判断上一个请求中是否含有该站的域名或ip地址,如果没有,则为非法请求,不是该站内的请求,
所以判断为盗链。两种判断方法
a:context.Request.UrlReferrer.Host.Contains("121.42.223.23")
b:context.Request.ServerVariables["HTTP_REFERER"].Contains("121.42.223.23")
3. 自定义hander进行处理
①. 自定义imgHandle,实现IHttpHandler接口
1 /// <summary> 2 /// 图片防盗链 3 /// 该自定义handle用来处理各种获取图片的请求 4 /// (这里处理 jpg gif png三种格式的图片) 5 /// </summary> 6 public class imgHandle : IHttpHandler 7 { 8 9 public bool IsReusable 10 { 11 // 如果无法为其他请求重用托管处理程序,则返回 false。 12 // 如果按请求保留某些状态信息,则通常这将为 false。 13 get { return true; } 14 } 15 16 public void ProcessRequest(HttpContext context) 17 { 18 19 //0.获取图片的返回类型 20 string type = GetContentType(context.Request.Url.ToString()); 21 22 var beforeUrl = context.Request.ServerVariables["HTTP_REFERER"]; 23 24 //1. 如果请求的url为空,或者请求的主机部分为空,返回一张禁止倒链的图片 25 if (context.Request.UrlReferrer == null || context.Request.UrlReferrer.Host == null) 26 { 27 //context.Response.ContentType = "image/JPEG"; 28 //context.Response.WriteFile("/Content/imageHandle/Forbidden.jpg"); 29 30 context.Response.Write("您的请求是非法,请勿再试"); 31 } 32 else 33 { 34 //2. url不空,且包含自己主机域名,表示为自己网站的请求,显示正常图片 35 //正常发布的时候这里用域名或者ip,localhost为了本地调试 36 if (context.Request.UrlReferrer.Host.Contains("121.42.200.127")) 37 { 38 string FileName = context.Server.MapPath(context.Request.FilePath); 39 context.Response.ContentType = type; 40 context.Response.WriteFile(FileName); 41 } 42 else 43 { 44 //3. url不空,但不包含自己主机域名,表示该请求为盗链请求,返回一张禁止倒链的图片 45 //context.Response.ContentType = "image/JPEG"; 46 //context.Response.WriteFile("/Content/imageHandle/Forbidden.jpg"); 47 48 context.Response.Write("您的请求是非法,请勿再试"); 49 } 50 51 } 52 } 53 54 /// <summary> 55 /// 将地址转换成图片返回值 56 /// </summary> 57 /// <param name="url"></param> 58 /// <returns></returns> 59 private static string GetContentType(string url) 60 { 61 switch (Path.GetExtension(url)) 62 { 63 case ".gif": 64 return "Image/gif"; 65 case ".jpg": 66 return "Image/jpeg"; 67 case ".png": 68 return "Image/png"; 69 default: 70 break; 71 } 72 return null; 73 } 74 75 }
扩展:也可以自定义handleFactory,实现IHttpHandlerFactory接口,在handleFactory中指定不同hander。
1 /// <summary> 2 /// 自定义的一个HandleFactory,用来指定不同的后缀调用不同的Handle 3 /// 这里是一个ImageHandlerFactory,处理不同 图片后缀 4 /// </summary> 5 public class ImageHandlerFactory : IHttpHandlerFactory 6 { 7 public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated) 8 { 9 string path = context.Request.PhysicalPath; 10 if (Path.GetExtension(path).Equals(".jpg")) 11 { 12 return new imgHandle(); 13 } 14 else if (Path.GetExtension(path).Equals(".png")) 15 { 16 return new imgHandle(); 17 } 18 else 19 { 20 //这里可以继续扩展,这里测试不继续扩展了 21 return new imgHandle(); 22 } 23 24 } 25 26 public void ReleaseHandler(IHttpHandler handler) 27 { 28 29 } 30 }
②. 在Web.config文件中的 <system.webServer>→<handlers>下添加:
<add name="img" path="*.jpg" verb="*" type="Ypf.Web.Core.PipeLine.imgHandle,Ypf.Web.Core" />
<add name="png" path="*.png" verb="*" type="Ypf.Web.Core.PipeLine.imgHandle,Ypf.Web.Core" />
<add name="gif" path="*.gif" verb="*" type="Ypf.Web.Core.PipeLine.imgHandle,Ypf.Web.Core" />
1 <!--VS2013及以后/IIS7.0之后的集成模式 需要添加下面的system.webServer节点--> 2 <system.webServer> 3 4 <!--1. 在此处配置modules--> 5 <modules runAllManagedModulesForAllRequests="false"> 6 <!--1.1 runAllManagedModulesForAllRequests处理静态文件的请求--> 7 <remove name="FormsAuthentication" /> 8 <!--1.2 优化网站性能,去掉不需要的module--> 9 <remove name="RoleManager" /> 10 <remove name="FileAuthorization" /> 11 <remove name="UrlAuthorization" /> 12 </modules> 13 <!--2. 在此处配置handlers--> 14 <handlers> 15 <!--2.1 优化网站性能,去掉不需要的module--> 16 <remove name="ExtensionlessUrlHandler-Integrated-4.0" /> 17 <remove name="OPTIONSVerbHandler" /> 18 <remove name="TRACEVerbHandler" /> 19 <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" /> 20 21 <!--2.2 添加自定义handlers--> 22 <!--2.2.1 后缀为ypf的请求 --> 23 <add name="ypf" path="*.ypf" verb="*" type="Ypf.Web.Core.PipeLine.ypfCode,Ypf.Web.Core" /> 24 25 <!--2.2.2 图片处理请求 --> 26 <!--方案一 逐个添加不同格式的图片 特别注意:不支持在一个path写多个扩展名--> 27 <add name="img" path="*.jpg" verb="*" type="Ypf.Web.Core.PipeLine.imgHandle,Ypf.Web.Core" /> 28 <add name="png" path="*.png" verb="*" type="Ypf.Web.Core.PipeLine.imgHandle,Ypf.Web.Core" /> 29 <add name="gif" path="*.gif" verb="*" type="Ypf.Web.Core.PipeLine.imgHandle,Ypf.Web.Core" /> 30 <!--方案二 通过HangerFactory来处理 特别注意:不支持在一个path写多个扩展名 --> 31 <!--<add name="img" path="*.jpg" verb="*" type="Ypf.Web.Core.PipeLine.ImageHandlerFactory,Ypf.Web.Core" /> 32 <add name="png" path="*.png" verb="*" type="Ypf.Web.Core.PipeLine.ImageHandlerFactory,Ypf.Web.Core" /> 33 <add name="gif" path="*.gif" verb="*" type="Ypf.Web.Core.PipeLine.ImageHandlerFactory,Ypf.Web.Core" />--> 34 35 </handlers> 36 </system.webServer>
4. 代码测试
新建一个Index页面,在该页面通过img标签直接链接到图片地址,显示图片,然后将该项目发布到测试服务器上。
①:访问地址:http://121.42.200.127:8099/Pipe/Index, 正常显示该页面中的图片, 表示该图片请求为站内正常的请求
②:访问地址:http://121.42.200.127:8099/Content/imageHandle/pipe1.jpg, 不能正常访问,表示该图片请求为盗链
第十二节:MVC中的一些特殊优化
一. 删除WebForm视图引擎
在MVC框架中检索视图的顺序为:当前控制器下对应的文件夹的aspx文件→share文件夹aspx文件→当前控制器下对应文件夹的cshtml文件→share文件夹的cshtml文件。
鉴于以上顺序,如果我们使用的MVC5框架中不需要WebForm的视图引擎,可以删除,来提高视图的检索速度。
同样是在Global.asax中操作,代码如下:
1 public class MvcApplication : System.Web.HttpApplication 2 { 3 /// <summary> 4 /// 删除WebForm视图即aspx文件视图检索引擎 5 /// </summary> 6 protected void RemoveWebFormEngines() 7 { 8 var viewEngines = ViewEngines.Engines; 9 var webFormEngines = viewEngines.OfType<WebFormViewEngine>().FirstOrDefault(); 10 if (webFormEngines != null) 11 { 12 viewEngines.Remove(webFormEngines); 13 } 14 } 15 16 17 /// <summary> 18 /// 网站第一次启动的时候会率先执行,且只执行一次 19 /// </summary> 20 protected void Application_Start() 21 { 22 AreaRegistration.RegisterAllAreas(); //区域 23 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); //过滤器 24 RouteConfig.RegisterRoutes(RouteTable.Routes); //常规路由 25 BundleConfig.RegisterBundles(BundleTable.Bundles); //js包、css包、combres 26 27 //删除WebForm视图即aspx文件视图检索引擎 28 RemoveWebFormEngines(); 29 //阻止MVC将版本号发回给浏览器 30 MvcHandler.DisableMvcResponseHeader = true; 31 //注册自定义实例化控制器的容器 32 ControllerBuilder.Current.SetControllerFactory(new UnityControllerFactory()); 33 } 34 }
二. 隐藏MVC版本号
在Asp.Net MVC框架中,默认打开一个页面,会将MVC的版本显示在浏览器上,如下:
在Global.asax中的Application_Start()方法中添加一句话:MvcHandler.DisableMvcResponseHeader = true; 即可阻止将MVC版本暴露给浏览器,如下:
第十一节:Bundles压缩合并js和css及原理分析
一. 简介
1.背景:浏览器默认一次性请求的网络数是有上限的,如果你得js和css文件太多,就会导致浏览器需要多次加载,影响页面的加载速度, MVC中提供Bundles的方式压缩合并js和css,是MVC中特有的一种优化方式。
(当然现在前端也有很多基于node的工作流插件,可以合并压缩混淆js或css)
2. 原理(了解即可):核心方法ApplyTransforms,通过StreamReader不断一次一次进行读取
参考文档:https://docs.microsoft.com/en-us/aspnet/mvc/overview/performance/bundling-and-minification
二. 如何使用
①:首先要在Global中进行注册,代码:BundleConfig.RegisterBundles(BundleTable.Bundles); 【系统默认已经注册,此步骤不需要我们操作】
②:在BundleConfig类中进行自定义合并压缩js或css
a. 压缩css方法:bundles.Add(new StyleBundle("~/XXX").Include("","",""));
StyleBundle中的参数为一个虚拟路径,单必须以【~/】开头,后面自定义,该名称即为在前端页面引入的名称。
Include中的参数为一个可变的string数组,用来声明要压缩合并的css文件的路径。
b. 压缩js方法:bundles.Add(new ScriptBundle("~/XXX").Include("","",""));
ScriptBundle中的参数为一个虚拟路径,单必须以【~/】开头,后面自定义,该名称即为在前端页面引入的名称
Include中的参数为一个可变的string数组,用来声明要压缩合并的css文件的路径。
③:在前端页面通过Render方法或者原生方法引入合并后的js或css的虚拟目录
④:将webconfig中的调试模式设置为false,这样在不发布的情况下就可以看到压缩后的效果。
<compilation debug="false" targetFramework="4.5" />
如果设置为true,直接在vs中运行,是看不到压缩效果的,仍然是多个js或css文件 (适用于通过Render方式的引入)
⑤:该步骤和④的效果相同.
如果不设置④的情况下,还想在vs中运行看到压缩效果,也可以在BundleConfig中加一句话:
BundleTable.EnableOptimizations = true; (特别注意:这里是true,而配置文件的方式是设置为false)
(适用于通过Render方式的引入)
补充:通过表达式扩展:(详细语法可以参考上面的地址,下面扩展几个常用的)
①: T* 该目录下以T开头的js或css文件
②: *.js *.css 该目录下的所有js或css文件
③: T*.js T*.css 这个是不合法,违规的
三. 代码测试
1. 新建两个js文件和css文件
2. 在BundleConfig文件中进行注册
3. 编写前端页面,以传统的方式引入,并查看效果。
4. 引入压缩合并后的路径。
方式一的运行结果:
方式二的运行结果:发现并没有压缩
至于为什么,上面已经解释了。
解决方案:
方案①:BundleTable.EnableOptimizations = true;
方案②:<compilation debug="false" targetFramework="4.5" />
重新运行:
最后补充几个含格式验证的压缩:
js压缩后产生的分号,是因为Razor语法不需要以分号结尾
第十节:数据批注(DataAnnotationModel)和自定义验证(包括Model级别的验证)
一. 简介
写完上一个章节MVC中的常用特性,迫不及待将该系列补全,该章节主要介绍数据批注(也叫:注解)。
一听【数据批注】,好高大上的名字,但仔细一看,它们其实是【System.ComponentModel.DataAnnotations】程序集下的一些特性类,O(∩_∩)O哈哈~,既然是特性,就符合特性的所有特征,只不过这些特性是作用于“属性”上的。
再一看【System.ComponentModel.DataAnnotations】这个命名空间,有点眼熟,与之前EF中的一篇文章【EF的CodeFirst模式通过DataAnnotations修改默认协定】中的一类操作来源于同一个命名空间下。
所以综上所述:该命名空间下的特性,在EF中可以用来映射生成数据库中的表字段,在日常开发中也可以用于做类中属性的限制和验证。
原理:均继承了ValidationAttribute特性,通过覆写IsValide方法进行校验。
适用场景:很多项目需要客户端和服务器端进行双重格式验证,使之更加安全,这时服务器端就可以使用数据批注了来进行校验了。
以Required特性为例,查看一下源码:
二. 常用的数据批注
这里总结一下【System.ComponentModel.DataAnnotations】命名空间下常用的数据批注,即特性。
① Key :声明主键
② Required:非空声明
③ MinLength和MaxLength:设置string类型的最大长度和最小长度,数据库的对应nvarchar
④ StringLength:设置string类型的长度,数据库对应nvarchar
⑤ Compare:新老密码对比
⑥ RegularExpression:正则的匹配
⑦ Phone:验证手机号码
⑧ Range:验证范围
⑨ Timestamp:将byte[]类型设置为timestamp类型
⑩ ConcurrencyCheck:并发检查,执行update操作时,会检查并发性(乐观锁) (在后面并发章节着重介绍Timestamp和ConcurrencyCheck)
另外还有一些不是很常用的,如:
① DisplayName:声明属性的名称
② Remote:远程验证,需要JQuery插件的支持 (这里不做测试等待补充 参考: https://www.cnblogs.com/JustRun1983/p/3505151.html)
下面补充一下该命名空间反射源码,可以自行查找需要的批注:
代码测试:
(1). 实体类,在其属性上添加数据标注
1 /// <summary> 2 /// 用户信息类 ,用于测试框架本身提供的数据批注 3 /// </summary> 4 public class UserInfor 5 { 6 [Required] 7 public string id { get; set; } 8 9 [StringLength(4)] 10 public string userName { get; set; } 11 12 [MaxLength(8)] 13 public string userMsg { get; set; } 14 15 [Range(2, 10)] 16 public int userAge { get; set; } 17 18 [RegularExpression("[a-d]")] //a-d中的一个 19 public string userMsg3 { get; set; } 20 21 [Phone] 22 public string userPhone { get; set; } 23 24 public string userOldPwd { get; set; } 25 26 [Compare("userOldPwd")] //比较和userOldPwd的值是否相等 27 public string userNewPwd { get; set; } 28 29 }
(2). 前端代码
1 //1. 测试数据批注 2 $("#btn1").click(function () { 3 $.ajax({ 4 type: "Post", 5 url: "TestDataAnnotationModel", 6 data: { 7 "id":"123", 8 "userName": "mr12", 9 "userMsg": "ypf1234", 10 "userAge": 6, 11 "userMsg3": "a", 12 "userPhone": "15764222366", 13 "userOldPwd": "123456", 14 "userNewPwd":"123456" 15 16 }, 17 success: function (data) { 18 if (data == "ok") { 19 alert("测试通过"); 20 } 21 if (data == "error") { 22 alert("测试未通过"); 23 } 24 } 25 }); 26 });
(3). 服务器端代码
1 public ActionResult TestDataAnnotationModel(UserInfor user) 2 { 3 //通过该方法进行验证 4 var isValidate = ModelState.IsValid; 5 6 if (isValidate) 7 { 8 return Content("ok"); 9 } 10 return Content("error"); 11 }
三. 自定义数据批注
思路:通过上面的批注源码可知,均为自定义类继承:ValidationAttribute,覆写IsValid方法
需求:这里我们自定义一个批注,要求不为空,且长度区间为6-12位
使用方法同样为:action中通过实体接收,通过ModelState.IsValid的值为true或false来判断验证是否通过
代码测试:
(1). 实体类和自定义批注
1 /// <summary> 2 /// 角色类,用于测试自定义业务的数据批注 3 /// </summary> 4 public class RoleInfor 5 { 6 7 public string id { get; set; } 8 9 [myOwnCheck] 10 public string roleName { get; set; } 11 } 12 13 /// <summary> 14 /// 自定义数据批注,要求非空且长度为6-12位 15 /// </summary> 16 public class myOwnCheckAttribute : ValidationAttribute 17 { 18 public override bool IsValid(object value) 19 { 20 if (value != null && value.ToString().Length > 6 && value.ToString().Length < 12) 21 { 22 return true; 23 } 24 return false; 25 } 26 }
(2). 前端代码
1 //2. 测试自定义业务逻辑的验证 2 $("#btn2").click(function () { 3 $.ajax({ 4 type: "Post", 5 url: "TestMyOwnCheck", 6 data: { 7 "id": "123", 8 "roleName": "mr12345" 9 }, 10 success: function (data) { 11 if (data == "ok") { 12 alert("测试通过"); 13 } 14 if (data == "error") { 15 alert("测试未通过"); 16 } 17 } 18 }); 19 });
(3). 服务器端代码
1 public ActionResult TestDataAnnotationModel(UserInfor user) 2 { 3 //通过该方法进行验证 4 var isValidate = ModelState.IsValid; 5 6 if (isValidate) 7 { 8 return Content("ok"); 9 } 10 return Content("error"); 11 }
四. Model级别的验证扩展
实现IValidaableObjec接口,实现Validate方法。(了解即可)
1 public class CarInfor: IValidatableObject 2 { 3 public string id { get; set; } 4 5 public string carName { get; set; } 6 7 public int carAge { get; set; } 8 9 IEnumerable<ValidationResult> IValidatableObject.Validate(ValidationContext validationContext) 10 { 11 if (carAge % 2 == 0) 12 { 13 var result = new ValidationResult("车龄验证不通过", new string[] { "carAge" }); 14 15 yield return result; 16 } 17 } 18 }
第九节:从源码的角度分析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 });
第五节:从源码的角度理解各种Result(ActionResult、JsonResult、JavaScriptResult等)
一. 背景
提到MVC不得不说MVC中的各种Result,这些高度封装的xxxResult以及在xxxResult再度封装的xxx,大大提高了MVC框架的开发效率。
相信做过MVC开发的朋友都会用到过 return Content("xx"), 给客户端的Ajax请求返值,那么Content内部是怎么实现的呢?Content和ContentResult之间又是什么关系呢?ContentResult内部又是怎么实现的呢?
与此类似的还有很多:Json方法和JsonResult、JavaScript方法和JavaScriptResult、Empty方法和EmptyResult 等等。
解决上面的问题之前,我们需要准备两件利器:
①:ILSpy代码反射工具(网上下载破解版)。
②:Reflector Vs的反射插件(通过Nuget下载)。
有了上面这两种利器中任何一种,就可以清晰的看到各种Result中的代码实现了。
下面以Content为例,简单介绍一下大致思路,在第二模块,将详细介绍每个Result。
①:Content方法是返回值为ContentResult类型的一个方法,即new了一个ContentResult类进行return,代码如下图:
②:ContentResult类内部又是怎么实现的呢?代码如下,最后核心代码是通过 response.write(""),向客户端返回了一个字符串。
总结:经过这两步,Content也好,ContentResult也好,原理就很清晰了,下面同样按照这个套路,详细的分析各种Result的内部原理,以及测试其使用情况。
二. 逐个分析
1. ActionResult
①:它是一个抽象类,且包含一个抽象方法ExecuteResult,各种Result都直接或间接继承ActionResult,并实现ExecuteResult方法。
②:应用于Action方法前面的类型,它是Action的返回值,代表Action的执行结果。
源码如下:
代码测试:指向页面
1 public ActionResult Index() 2 { 3 return View(); 4 }
2. JsonResult
①:继承了ActionResult,实现了ExecuteResult方法。
②:解读源码可知,JsonResult内部实现原理是调用了JavaScriptSerializer对象中的Serialize方法,将Json对象转换成了Json字符串,
通过:response.Write(javaScriptSerializer.Serialize(this.Data)); 传递给前台。
③:默认是进制Get请求访问的. JsonRequestBehavior.DenyGet。
④:在MVC的Action中,return Json(),这里的Json通过源码可知,即new了一个JsonResult对象而已,并且MVC中封装了很多重载。
⑤:应用于action充当APP接口的情况,且返回Json字符串,但ajax接受到后,会自动转换成Json对象进行使用。
源码如下:
代码测试:
1 /// <summary> 2 /// 通过Index1页面向该方法发送请求 3 /// 前端拿到的是JSON字符串,需要前端转换 4 /// </summary> 5 /// <returns></returns> 6 public ActionResult GetInfor() 7 { 8 var data =new 9 { 10 id="1", 11 name="mr" 12 }; 13 string data2 = new JavaScriptSerializer().Serialize(data); 14 return Json(data2, JsonRequestBehavior.AllowGet); 15 } 16 /// <summary> 17 /// 前端拿到的是JSON对象,前端可以直接调用 18 /// </summary> 19 /// <returns></returns> 20 public ActionResult GetInfor2() 21 { 22 var data = new 23 { 24 id = "1", 25 name = "mr" 26 }; 27 return Json(data, JsonRequestBehavior.AllowGet); 28 }
测试结果:一个返回了Json对象,一个返回了Json字符串
3. JavaScriptResult
①:继承了ActionResult,实现了ExecuteResult方法。
②:解读源码可知:JavaScriptResult内部实现原理,设置了返回参数类型 response.ContentType = "application/x-javascript";
然后通过: response.Write(this.Script);将js代码返回前台。
可以举一反三:通过ContentResult并且设置其类型为"application/x-javascript",可以达到ContentResult同样的效果。
③:在MVC的Action中,return JavaScript(),这里的JavaScript通过源码可知,即new了一个JavaScriptResult对象而已,并且MVC中封装了很多重载。
④:应用于通过后台返给前端js代码。
源码如下:
代码测试:下面两段代码的结果是一致的
1 public ActionResult GetJs() 2 { 3 return JavaScript("alert('我是js代码,调用的是JavaScriptResult')"); 4 } 5 public ActionResult GetJs2() 6 { 7 return Content("alert('我是js代码,调用的是ContentResult,并自定义的返回类型为js')", "application/x-javascript"); 8 }
4. ContentResult
①:继承了ActionResult,实现了ExecuteResult方法。
②:解读源码可知,ContentResult内部实现原理直接将数据通过这个方法Response.Write(string s)直接返回。
③:在MVC的Action中,return Content(),这里的Content通过源码可知,即new了一个ContentResult对象而已,并且MVC中封装了很多重载,可以手动设置的返回类型。
④:应用于返回一个简单的判断字段,默认表示一个文本内容,如下面的例子。
代码测试:下面两段代码效果原理是一致的。
1 /// <summary> 2 /// 下面这两种情况达到的效果是一致的,原理也一致 3 /// </summary> 4 public void GetInfor3() 5 { 6 Response.Write("ok"); 7 } 8 public ActionResult GetInfor4() 9 { 10 return Content("ok"); 11 }
5. EmptyResult
①:继承了ActionResult,实现了ExecuteResult方法。
②:解读源码可知:EmptyResult内部实现原理,实际上它的ExecuteResult方法中为空,什么也没有。
③:在MVC的Action中,return Empty(),这里的Empty通过源码可知,即new了一个EmptyResult对象而已,并且MVC中封装了很多重载。
④:这里的EmptyResult起到一个适配器作用,一个中转的作用,可以应用于请求不需要显示页面的情况。
6. RedirectResult
①:继承了ActionResult,实现了ExecuteResult方法。
②:解读源码可知:RedirctResult内部实现原理,实际上它的ExecuteResult方法调用的是context.HttpContext.Response.Redirect(text, false)。
③:在MVC的Action中,return Redirct(),这里的Redirct通过源码可知,即new了一个RedirctResult对象而已。
④:应用于在后台进行跨站点跳转和同站点间action之间进行跳转。
源码如下:
代码测试:同站点和跨站点间的跳转。
1 /// <summary> 2 /// 跨站点跳转 3 /// </summary> 4 /// <returns></returns> 5 public ActionResult RedirctToBaidu() 6 { 7 return Redirect("http://www.baidu.com"); 8 } 9 /// <summary> 10 /// 同站点间action之间进行跳转 11 /// </summary> 12 /// <returns></returns> 13 public ActionResult RedictOtherAction() 14 { 15 return Redirect("/Third/GetInfor4"); 16 }
7. RedirectToRouteResult
①:也是与页面跳转相关的。
②:mvc中 return RedirectToAction(""); return RedirectToRoute();都是跳转的一些变种,这里不再详细分析了。
8. FileResult
①:继承了ActionResult,实现了ExecuteResult方法。
②:解读源码可知:FileResultResult内部实现原理,实际上它的ExecuteResult方法调用的是WriteFile(response)。
③:MVC的Action中,return File(),即new了一个FileResult对象而已.有多个重载。
④:应用于下载文件,验证码的例子。
三. 自己扩展
1. 扩展一个高效的json序列化
2. 扩展一个xml序列化
3. 扩展一个ExcelResult,是传入一组数据,直接下载excel文件
四. 重点研究(ViewResult)(待补充)
第四节:MVC中AOP思想的体现(四种过滤器)并结合项目案例说明过滤器的实际用法
一. 简介
MVC中的过滤器可以说是MVC框架中的一种灵魂所在,它是MVC框架中AOP思想的具体体现,所以它以面向切面的形式无侵入式的作用于代码的业务逻辑,与业务逻辑代码分离,一经推出,广受开发者的喜爱。
那么过滤器到底是什么呢?它又有什么作用呢?
用户通过URL访问Web系统不一定都能得到相应的内容,一方面不同的用户权限不同,另一方面是为了保护系统,防止被攻击,这就是过滤器的核心所在,我们总计一下过滤器都有哪些作用:
①:判断用户是否登录以及不同用户对应不同的权限问题。
②:防盗链、防爬虫。
③:系统中语言版本的切换(本地化和国际化)。
④:权限管理系统中动态Action。
⑤:决策输出缓存。
知道到了过滤器的作用,那么过滤器分哪几类呢?如下图1:
二. 执行顺序
从上图①可知,过滤器分四类,总共重写了六个方法,在这六个方法里可以处理相应的业务逻辑,那么如果四种过滤器的六个重写方法同时存在,它们的执行顺序是什么呢?
首先要将OnException方法除外,该方法不和其余五个方法参与排序问题,该方法独立存在,什么时间报错,什么时候调用。
其余三种过滤器中的五个重写方法的执行顺序:
三. 自定义实现形式
1. 直接在控制器中重写方法或者利用控制器间的继承
新建任何一个控制器,它均继承Controller类,F12进入Controller类中,发现Controller类中已经实现了过滤器需要实现的接口,并且提供虚方法供我们重写,代码如下:
基于以上原理,这样在控制器级别上我们就有两种思路来实现过滤器。
方案一:直接在当前控制器重写相应的过滤器方法,则该过滤器的方法作用于当前控制器的所有Action。
方案二:新建一个父类控制器,在父类控制器中重写过滤器的方法,然后子类控制器继承该父类控制器,则该该过滤器作用于子类控制器中的所有Action。
【该方法和接下来以特性的形式作用于控制器的效果是一致的】
1 /// <summary> 2 /// 控制器继承该控制器,和特性作用在控制器上效果一致 3 /// </summary> 4 public class MyBaseFilterController : Controller 5 { 6 //需要用protected类型,不能用public类型 7 protected override void OnAuthorization(AuthorizationContext filterContext) 8 { 9 //1.如果保留如下代码,则会运行.net framework定义好的身份验证,如果希望自定义身份验证,则删除如下代码 10 // base.OnAuthorization(filterContext); 11 12 //2.获取区域名字 13 // string strAreaName = filterContext.RouteData.DataTokens["area"].ToString().ToLower(); 14 15 //3.获取控制器作用的Controller和action的名字 16 string controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName.ToLower(); 17 string actionName = filterContext.ActionDescriptor.ActionName.ToLower(); 18 filterContext.HttpContext.Response.Write("身份验证过滤器作用于" + controllerName + "控制器下的" + actionName + "方法</br>"); 19 } 20 }
2. 自定义类继承MVC中过滤器实现类或过滤器接口,特性的形式作用于控制器或Action
特别补充:MVC框架中的AuthorizeAttirbute、ActionFilterAttribute、HandleErrorAttribute类已经实现了过滤器对应的接口,所以我们在自定义过滤器的时候,可以直接继承以上三个类;或者实现相应的接口:IAuthorizationFilter、IActionFilter、IResultFilter、IExceptionFilter。(该方案在实现相应接口的同时,需要继承FilterAttribute,使自定义的类成为一个特性)。
下面以继承MVC中实现类的形式来自定义四种过滤器:
A:身份验证过滤器
1 /// <summary> 2 /// 身份验证过滤器 3 /// 1. 在非MVC框架项目中使用MVC过滤器,需要通过nuget把MVC的程序集添加进去 4 /// 2. 继承AuthorizeAttribute类,然后对OnAuthorization方法进行 override 覆写 5 /// 3. 在Action运行之前首先运行该过滤器 6 /// </summary> 7 public class MyAuthorize : AuthorizeAttribute 8 { 9 public override void OnAuthorization(AuthorizationContext filterContext) 10 { 11 //1.如果保留如下代码,则会运行.net framework定义好的身份验证,如果希望自定义身份验证,则删除如下代码 12 // base.OnAuthorization(filterContext); 13 14 //2.获取区域名字 15 // string strAreaName = filterContext.RouteData.DataTokens["area"].ToString().ToLower(); 16 17 //3.获取控制器作用的Controller和action的名字 18 string controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName.ToLower(); 19 string actionName = filterContext.ActionDescriptor.ActionName.ToLower(); 20 filterContext.HttpContext.Response.Write("身份验证过滤器作用于" + controllerName + "控制器下的" + actionName + "方法</br>"); 21 } 22 }
B: 行为过滤器
1 /// <summary> 2 /// 行为过滤器 3 /// 1. 在非MVC框架项目中使用MVC过滤器,需要通过nuget把MVC的程序集添加进去 4 /// 2. 继承ActionFilterAttribute类,然后对OnActionExecuting方法和OnActionExecuted方法进行 override 覆写 5 /// 3. OnActionExecuting方法:在action方法运行之前,且OnAuthorization过滤器运行之后调用 6 /// OnActionExecuted方法:在action方法运行之后调用 7 /// </summary> 8 public class MyAction: ActionFilterAttribute 9 { 10 11 /// <summary> 12 /// 在action方法运行之前调用 13 /// </summary> 14 /// <param name="filterContext"></param> 15 public override void OnActionExecuting(ActionExecutingContext filterContext) 16 { 17 //1.如果保留如下代码,则会运行.net framework定义好的行为验证,如果希望自定义行为验证,则删除如下代码 18 // base.OnActionExecuting(filterContext); 19 20 //2.获取区域名字 21 // string strAreaName = filterContext.RouteData.DataTokens["area"].ToString().ToLower(); 22 23 //3.获取控制器作用的Controller和action的名字 24 string controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName.ToLower(); 25 string actionName = filterContext.ActionDescriptor.ActionName.ToLower(); 26 filterContext.HttpContext.Response.Write("行为过滤器OnActionExecuting作用于" + controllerName + "控制器下的" + actionName + "方法运行之前</br>"); 27 } 28 /// <summary> 29 /// 在action方法运行之后调用 30 /// </summary> 31 /// <param name="filterContext"></param> 32 public override void OnActionExecuted(ActionExecutedContext filterContext) 33 { 34 //1.如果保留如下代码,则会运行.net framework定义好的行为验证,如果希望自定义行为验证,则删除如下代码 35 // base.OnActionExecuted(filterContext); 36 37 //2.获取区域名字 38 // string strAreaName = filterContext.RouteData.DataTokens["area"].ToString().ToLower(); 39 40 //3.获取控制器作用的Controller和action的名字 41 string controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName.ToLower(); 42 string actionName = filterContext.ActionDescriptor.ActionName.ToLower(); 43 filterContext.HttpContext.Response.Write("行为过滤器OnActionExecuted作用于" + controllerName + "控制器下的" + actionName + "方法运行之后</br>"); 44 } 45 }
C:结果过滤器
1 /// <summary> 2 /// 结果过滤器 3 /// 1. 在非MVC框架项目中使用MVC过滤器,需要通过nuget把MVC的程序集添加进去 4 /// 2. 继承ActionFilterAttribute类,然后对OnResultExecuting方法和OnResultExecuted方法进行 override 覆写 5 /// 3. OnResultExecuting方法:在执行结果之后(action之后),页面渲染之前调用 6 /// OnResultExecuted方法:在页面渲染之后调用 7 /// </summary> 8 public class MyResult : ActionFilterAttribute 9 { 10 11 /// <summary> 12 /// action执行之后(OnActionExecuting之后),页面渲染之前调用 13 /// </summary> 14 /// <param name="filterContext"></param> 15 public override void OnResultExecuting(ResultExecutingContext filterContext) 16 { 17 //1.如果保留如下代码,则会运行.net framework定义好的结果验证,如果希望自定义结果验证,则删除如下代码 18 // base.OnResultExecuting(filterContext); 19 20 //该方法中无法获取是哪个控制器后 21 filterContext.HttpContext.Response.Write("结果过滤器OnResultExecuting作用于action运行之后,页面加载之前"); 22 } 23 /// <summary> 24 /// 页面渲染之后调用 25 /// </summary> 26 /// <param name="filterContext"></param> 27 public override void OnResultExecuted(ResultExecutedContext filterContext) 28 { 29 //1.如果保留如下代码,则会运行.net framework定义好的结果验证,如果希望自定义结果验证,则删除如下代码 30 // base.OnResultExecuted(filterContext); 31 32 //该方法中无法获取是哪个控制器后 33 filterContext.HttpContext.Response.Write("结果过滤器OnResultExecuted作用于页面渲染之后"); 34 } 35 }
D:异常过滤器
使用自定义异常处理,需要在web.config中为system.web添加<customErrors mode="On" />节点
1 /// <summary> 2 /// 异常过滤器 3 /// 需要注意的点: 4 /// ①:如果自定义异常过滤器且需要有作用于全局,需要把FilterConfig中的 filters.Add(new HandleErrorAttribute());注释掉, 5 /// 然后把自定义的异常过滤器添加到FilterConfig中。 6 /// ②:使用自定义异常处理,需要在web.config中为system.web添加<customErrors mode="On" />节点 7 /// </summary> 8 public class MyException: HandleErrorAttribute 9 { 10 public override void OnException(ExceptionContext filterContext) 11 { 12 //调用框架本身异常处理器的方法 13 base.OnException(filterContext); 14 15 //获取异常信息(可以根据实际需要写到本地或数据库中) 16 var errorMsg = filterContext.Exception; 17 18 //跳转指定的错误页面 19 filterContext.Result = new RedirectResult("/error.html"); 20 } 21 }
下面展示以特性的形式作用于控制器或控制器中的Action:
3. 自定义类继承MVC中实现类或接口,全局注册,作用于全部控制器
如果以上两种方式均不能满足你的过滤器的使用范围,你可以在App_Start文件夹下的FilterConfig类中进行全局注册,使该过滤器作用于所有控制器中所有Action方法。
特别注意的一点是:自定义异常过滤器,需要把系统默认的filters.Add(new HandleErrorAttribute());注释掉。
全局注册的代码如下:
1 public class FilterConfig 2 { 3 public static void RegisterGlobalFilters(GlobalFilterCollection filters) 4 { 5 //如果自定义异常过滤器,需要把默认的异常过滤器给注释掉 6 //filters.Add(new HandleErrorAttribute()); 7 8 //自定义异常过滤器 9 filters.Add(new MyException()); 10 11 //全局注册身份验证、行为、结果过滤器 12 //filters.Add(new MyAuthorize()); 13 //filters.Add(new MyAction()); 14 //filters.Add(new MyResult()); 15 16 //全局注册登录验证(暂时注释,使用的时候要打开) 17 //filters.Add(new CheckLogin()); 18 } 19 }
四. 结合实际案例进行代码测试
1. 测试过滤器的执行顺序
将上面的身份验证过滤器、行为过滤器、结果过滤器以特性的形式作用于Action上,通过断点监控或者查看最后的输出结果:
结果:
符合:OnAuthorization→OnActionExecuting-> Action方法执行 ->OnActionExecuted->OnResultExecuting/ -> Render View() (页面渲染加载)->OnResultExecuted() 这一顺序。
2. 全局捕获异常,记录错误日志案例
步骤1:编写异常过滤器,通过 var errorMsg = filterContext.Exception; 获取异常信息,可以写入文本、存入数据库、或者是Log4Net错误日志框架进行处理。代码在上面。
步骤2:在web.config中为system.web添加<customErrors mode="On" />节点。
步骤3:添加到全局注册文件中进行捕获。
步骤4:在自定义的异常过滤器中添加断点,并且自己制造一个错误。
捕获到错误,进行页面跳转。
3. 登录验证案例
业务背景:在90%以上的Web系统中,很多页面都是登录成功以后才能看到的,当然也有很多页面不需要登录,对于需要登录才能看到的页面,即使你知道了访问地址,也是不能访问的,会退出到登录页面提示让你登录,对于不需要登录的页面通过URL地址可以直接访问。
分析:针对以上背景,过滤器对于大部分Action是需要过滤的,需要做登录验证,对于一小部分是不需要过滤的。
解决思路:
①:自定义一个身份验证过滤器,进行全局注册。
②:自定义一个Skip特性,将该特性加到不需要身份验证的Action上。
③:重点说一下身份证过滤器中的逻辑:
a. 先判断该Action上是否又Skip特性,如果有,停止不继续执行;如果没有,继续下面的验证逻辑。
b. 从Session中或Redis中读取当前用户,查看是否为空,如果为空,表明没有登录,返回到登录页面;如果不为空,验证通过,进行后面的业务逻辑。
代码段如下:
1 /// <summary> 2 /// 校验系统是否登录的过滤器 3 /// 使用身份验证过滤器进行编写 4 /// </summary> 5 public class CheckLogin: AuthorizeAttribute 6 { 7 public override void OnAuthorization(AuthorizationContext filterContext) 8 { 9 //1. 校验是否标记跨过登录验证 10 if (filterContext.ActionDescriptor.IsDefined(typeof(skipAttribute), true) 11 || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(skipAttribute), true)) 12 { 13 //表示该方法或控制器跨过登录验证 14 return; 15 } 16 //2. 校验是否登录 17 //可以使Session或数据库或nosql 18 //这里只是测试,所有统统当做没有登录来处理 19 var sessionUser = HttpContext.Current.Session["CurrentUser"];//使用session 20 if (sessionUser == null) 21 { 22 HttpContext.Current.Session["CurrentUrl"] = filterContext.RequestContext.HttpContext.Request.RawUrl; 23 //如果没有登录,则跳转到错误页面 24 filterContext.Result = new RedirectResult("/error.html"); 25 } 26 } 27 }
第三节:Action向View传值的四种方式(ViewData、ViewBag、TempData、Model)
简 介
在前面的章节中,我们已经很清楚,MVC工作模型的流程,Controller中的Action接收到客户端的请求,处理后要将数据返回给View,那么Action中是如何将数据返回给View的,二者之间打通的桥梁又是什么呢?
这些问题正是本章节需要解决的,下面先复习一下MVC的请求模型,如下图:
(一). 先解决第一个问题,Action向View中传值有四种方式:ViewData、ViewBag、TempData、Model,随意选中一个点击F12查看源码,源码如下:
分析:ViewData和TempData分别是ViewDataDictionary类型和TempDataDictionary类型,而这两种类型均实现了IDictionary接口,所以ViewData和TemData均为字典类型。
我们再看一下ViewDataDictionary和TempDataDictionary两个类型源码,以ViewDataDictionary为例,代码如下:
分析可知赋值方式有两种: ViewData[" "]=XXX 和 ViewData.Add("key", value) ,TempData赋值方式与此类似。
(二). 接下来我们解决第二个问题,在前端页面选择ViewData点击F12,查看WebViewPage类源码,源码如下:这时候,应该都很清楚了。
(三). 总结一下结论:
A. ViewData:字典类型,在前端页面使用的时候,需要进行类型转换。
B. ViewBag:动态类型,运行时自动进行类型转换,不需要进行任何类型转换。
C:Model: 实质就是ViewData.Model,前端页面通过Model.XXX进行调用,页面需要using引入程序集。
D:TempData:字典类型,前端页面使用时候,需要进行类型转换,但该类型更多的是作为临时变量应用于后台Action直接的传值,它内部是基于Session实现的,它可以存储一次,但是只能读取一次,再次使用,将为空。
1. 测试四种方式向页面传值
1 public ActionResult PassValueIndex() 2 { 3 ViewData["num"] = 2; 4 ViewData.Add("num2", 2); 5 6 ViewBag.myNum = 2; 7 TempData["myNum2"] = 2; 8 9 Student stu = new Student() 10 { 11 id="123456", 12 name="ypf", 13 sex="男" 14 }; 15 return View(stu); 16 }
1 @*使用Model赋值,需要引入下面的命名空间*@ 2 @using Ypf.MVC5.Models; 3 @{ 4 Layout = null; 5 } 6 7 <!DOCTYPE html> 8 9 <html> 10 <head> 11 <meta name="viewport" content="width=device-width" /> 12 <title>PassValueIndex</title> 13 </head> 14 <body> 15 <div> 16 <p>Model赋值: 17 @Model.id 18 @Model.name 19 @Model.sex 20 </p> 21 <p>ViewData(需要进行类型转换):@((int)ViewData["num"]+1) </p> 22 <p>ViewData(需要进行类型转换):@((int)ViewData["num2"] + 1) </p> 23 24 <p>ViewBag(不需要进行类型转换):@(ViewBag.myNum+1)</p> 25 <p>TempData(需要进行类型转换):@((int)TempData["myNum2"]+1)</p> 26 <a href="TestTempData1">第二次调用TempData</a> 27 </div> 28 </body> 29 </html>
2. 测试TempData的时效性
从上面页面前端代码中点击,a标签,进入下面代码:
1 public ActionResult TestTempData1() 2 { 3 //第二次次测试使用TempData,因为PassValueIndex页面已经使用了一次, 4 //所以此处data1为空,很好的印证了TempData只能调用一次的结论 5 var data1 = TempData["myNum2"]; 6 return Content(""); 7 }
分析发现,这里的data1为null,印证了TempData使用一次后清空的结论。
第二节:各种路由约束(动态路由、静态路由、组合路由、正则约束、命名空间约束、区域内路由)
一. 什么是路由
路由是约束URL的一组规范,那么什么是URL呢?通俗的来说URL是一个地址,通过该地址,用户可以访问Web网站或者下载服务器上的文件。
比如下面就是两组URL:
http://www.cnblogs.com/yaopengfei/p/7828441.html
http://www.cnblogs.com/yaopengfei/p/7828441
显然我们喜欢第二组,省略.html,会使该地址看起来更加简洁,更加友好,利于SEO优化。
那么我们怎么实现这个简单的需求呢?
答案是:通过【路由】配置,所以现在我们似乎有点清晰了,路由可以规定URL的特殊格式,使其达到特殊效果。
在ASP.NET MVC框架中,通过路由配置URL,使用户的URL请求可以映射到Controller下的action方法中,执行相应操作,并接受URL中传过来的参数,在MVC5框架中,在【RouteConfig.cs】类中进行路由规则的配置,如下图:
二. 从源码的角度分析路由
我们进入【RouteConfig】类中,发现核心代码是调用【RouteCollection】类下的MapRoute方法,F12看源码得知,MapRoute方法是RouteCollectionExtensions类的给【RouteCollection】类扩展方法的方法,并且有多个重载,如下图:
下面分析一下参数的含义:
(1) name: 要映射的路由的名称。
(2) url: 路由的 URL 模式,可以自定义路由的格式,可以写静态路由,也可以写动态路由、组合路由等。
(3) defaults: 一个包含默认路由值的对象,书写路由的默认值。
(4) constraints: 一组表达式,可以使用正则指定 url 参数值的约束。
(5) namespaces: 应用程序的一组命名空间,可以缩小检索路由对象匹配的范围。
底层源码,有兴趣可以看一下:
1 public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces) 2 { 3 if (routes == null) 4 { 5 throw new ArgumentNullException("routes"); 6 } 7 if (url == null) 8 { 9 throw new ArgumentNullException("url"); 10 } 11 Route route = new Route(url, new MvcRouteHandler()) { 12 Defaults = CreateRouteValueDictionaryUncached(defaults), 13 Constraints = CreateRouteValueDictionaryUncached(constraints), 14 DataTokens = new RouteValueDictionary() 15 }; 16 ConstraintValidation.Validate(route); 17 if ((namespaces != null) && (namespaces.Length > 0)) 18 { 19 route.DataTokens["Namespaces"] = namespaces; 20 } 21 routes.Add(name, route); 22 return route;
三. MVC中的几类路由及其规则
1. 动态路由
1 routes.MapRoute( 2 name: "Default", //路由名称 3 url: "{controller}/{action}/{id}", //路由规则 4 defaults: new { controller = "First", action = "Index1", id = UrlParameter.Optional } //默认值,当Controller或action为空(省略)的时候调用 5 );
分析:路由规则为 {controller}/{action}/{id} ,其中 {controller}、{action}、{id}为三个参数,/ 为格式分割符号,defaults中声明的是默认路由,所以下面的测试结果:
http://localhost:7559/
http://localhost:7559/First
http://localhost:7559/First/index1
http://localhost:7559/First/Index1/2
都会跳转到Index1的页面。
变种:将上面的代码的URL分割符号稍微调整一下
1 routes.MapRoute( 2 name: "Default9", //路由名称 3 url: "{controller}/{action}-{id}", //路由规则 4 defaults: new { controller = "First", action = "Index1", id = UrlParameter.Optional } //默认值,当Controller或action为空(省略)的时候调用 5 );
请求地址就变成了:
http://localhost:7559/ 【无法访问】
http://localhost:7559/First 【404找不到】
http://localhost:7559/First/index1 【404找不到】
http://localhost:7559/First/Index1-1 【可以访问】
2. 静态路由
1 routes.MapRoute( 2 name: "Default2", //路由名称 3 url: "Ypf", //路由规则,不区分大小写,当输入“ypf”时,会自动跳转到下面的地址 4 defaults: new { controller = "First", action = "Index1", id = UrlParameter.Optional } //默认值,当Controller或action为空的时候调用 5 );
静态路由:url中是一个静态值,访问的URL只有输入这个静态值,才能访问下面default中的默认值。
测试地址如下:
http://localhost:7559/ 【无法访问】
http://localhost:7559/ypf 【 跳转到index1页面】
http://localhost:7559/First/index1 【无法访问】
补充一下:MapRoute方法是可以不需要写参数名的,就像正常的调用方法一样,所以上面的代码可以改写:
1 routes.MapRoute( 2 "Default3", //路由名称 3 "Ypf", //路由规则,不区分大小写,当输入“Ypf”时,会自动跳转到下面的地址 4 new { controller = "First", action = "Index1", id = UrlParameter.Optional } //默认值,当Controller或action为空的时候调用 5 );
3. 组合路由
1 routes.MapRoute( 2 "Default4", //路由名称 3 "Ypf/{action}", //路由规则,不区分大小写,规则相符的时候,会自动跳转到下面的地址 4 new { controller = "First", action = "Index1" } 5 );
所谓的组合路由,就是静态路由和动态路由相互组合使用,测试地址如下:
http://localhost:7559/ 【无法访问】 (分析:因为不满足路由规则,没有输入ypf)
http://localhost:7559/ypf 【 跳转到index1页面】
http://localhost:7559/ypf/index1 【 跳转到index1页面】
http://localhost:7559/Ypf/hh 【404找不到】 (满足路由规则,但是没有hh这个action,所以404)
http://localhost:7559/ypf/First/index1 【404找不到】 (满足路由规则,但这里把First当做action,并没有这个action,所以404)
4. 正则约束
1 routes.MapRoute( 2 "Default5", 3 "{controller}/{action}_{Year}_{Month}_{Day}", 4 new { controller = "First", action = "Index1", id = UrlParameter.Optional }, 5 new { Year = @"^\d{4}", Month = @"\d{2}", Day = @"\d{2}" } 6 );//正则路由
所谓的正则约束,是指可以对URL中的参数使用正则表达式进行约束,上述代码约束了Year必须是四位数字,Month和Day必须是两位数字。
测试地址:
http://localhost:7559/first/index1_2018_09_01 【跳转到index1页面】
http://localhost:7559/first/index1 【无法访问】 (分析:因为不满足路由规则,没有输入{Year}_{Month}_{Day} 的参数)
http://localhost:7559/first/ 【无法访问】 (分析:因为不满足路由规则,没有输入{Year}_{Month}_{Day} 的参数)
http://localhost:7559/ 【无法访问】 (分析:因为不满足路由规则,没有输入{Year}_{Month}_{Day} 的参数)
5. 命名空间约束
1 routes.MapRoute( 2 name: "Default6", 3 url: "{controller}/{action}/{id}", 4 defaults: new { controller = "Third", action = "Index", id = UrlParameter.Optional }, 5 namespaces: new string[] { "Ypf.MVC5" } 6 );
所谓的命名空间约束,即限定匹配范围路由的检索范围,提高检索速度。
特别注意:不能从外层控制器直接跳转到内层Area内的控制器!!
测试地址:以下三个访问地址,都会跳转到index1页面
http://localhost:7559/
http://localhost:7559/First
http://localhost:7559/First/index1
6. Area区域内的路由
1 public override void RegisterArea(AreaRegistrationContext context) 2 { 3 //原路由 4 //context.MapRoute( 5 // "TestOne_default", 6 // "TestOne/{controller}/{action}/{id}", 7 // new { action = "Index", id = UrlParameter.Optional } 8 //); 9 10 //结合命名空间进行路由改造 11 context.MapRoute( 12 this.AreaName + "_default", 13 this.AreaName + "/{controller}/{action}/{id}", 14 new { area = this.AreaName, controller = "Sys_Admin", action = "Index", id = UrlParameter.Optional }, 15 new string[] { "Ypf.MVC5.Areas." + this.AreaName + ".Controllers" } 16 ); 17 18 }
指MVC5框架中Area区域单独的一套路由规则,我们可以结合区域内的原路由,进行改造一番,如上述代码。