ASP.NET MVC 3.0:基于Ajax的表单提交,A页面认证失败后页面被强转至登录页面,待登录成功将如何回到A页面?
很多网站的首页都提供信息的输入,而不论您是否有账户且已登录。比如我喜欢逛的42qu(我跟创始人无任何关系,仅是喜欢该网站且无意广告,有兴趣的可以瞧瞧去)。当我发表自己的"碎碎念"时,会被自动跳转到登录页,而问题是登录成功后能否再回到原来的页面。听起来这个问题略显乏善可陈,然而它的实现框架是MVC 3.0,而且为了寻求其优雅的实现方式尝试了很多天,现作为记要并分享一下。
先来一张最终效果图吧:
上图的页面切分为两个区域,一是碎碎念的发表区(上部分)、另一个是呈现区(下部分),以真分页提取数据;信息提交是基于Ajax.BeginForm()进行的,然后即时无刷新于呈现区。
看看这个基于异步提交的表单是如何陈述的:
值得关注的属性有UpdateTargetId,它预示着异步加载的数据将呈现于哪个Div;另外OnSuccess事件也尤为重要,它担当着回调的处理重任。最后,异步请求的数据想成功加载于指定的Div中,得引用 jquery.validate.unobtrusive.min.js脚本文件(第3行):
这个js文件的功能类似于ASP.NET AJAX中的"万能"控件UpdatePanel。
现在我想发表一条碎碎念,试图点击"碎碎念"进行发送,由于近一个星期没有登录,其相关Cookie已然过期,页面将被强转至登录页(Login.cshtml),跳转本身倒是我的设计意图,但页面呈现出的结果却很糟糕,因为整个登录页面全部呈现在id名为"ChatIntegrationContent"的Div内,其页面大家可以想像得出是很荒唐的,于是我要解决两个问题:
1.默认的认证属性AuthorizeAttribute已经不能满足我的认证需求了,我得自定义认证类LoginAuthorizeAttribute并继承它,且重写(override)相关受保护的虚方法(protected virtual method);
2.服务器端的任何跳转显然已是徒劳:无论是Redirect还是变向的模拟Server.Transfer模式来曲线跳转,都会乖乖地落入原UpdateTargetId属性指定的Div中。一开始我想,在服务端我能取消Ajax调用吗?使其转变成普通调用,即形同@Html.BeginForm()这种普通表单的Post请求。答案是NO!这个请求是Ajax异步产生的,其命运已然注定,故唯一的途径便是通知客户端脚本作window.location跳转!
祸源于Ajax异步调用,解决之道自然还要从它入手,且看看自定义认证类有如何了不得:
继承AuthorizeAttribute后,仅重写了虚函数:protected virtual void HandleUnauthorizedRequest(AuthorizationContext filterContext);这意味着形如:[Authorize(Roles = "Member")]这样的认证逻辑依旧采用MVC 3.0自己的算法,看了一下它的源码,确实比我自己另起灶炉写得全面而优雅得多,我羞愧的将自己实现的百来行代码统统删除,面向对象的可重用性真是棒啊。可以看到,上下文对象filterContext的属性Result已被设置成为JsonResult。另外有个内在原因要注意,Authorize属性(Attribute)认证失败后,是因为抛出了401异常,才导致MVC框架跳转到登录页(loginUrl属性所指定):
loginUrl属性显式指定了登录页,按照dudu老大的建议,cookieless也显式指定为 UseCookies,为测试方便Cookie和认证票据(Ticket)都只设置一分钟(timeout="1")。
上述代码最重要的一句是:filterContext.HttpContext.Request.IsAjaxRequest()
它拷问当前请求是否为异步的Ajax请求,之所以这么判断是因为客户端发来的假如是基于@Html.BeginForm()的普通请求,那服务端可以轻松跳转而刷新整个浏览器,不用担心登录页落入相关Div之内;相反Ajax请求则要加倍呵护,它得通知JS脚本作客户端跳转(window.location),方能保证登录页是刷新整个浏览器。以JSON格式封装一个键值对传回客户端:
继承自ActionResult的"JSON格式内容"类JsonResult,有一个专门用来装载数据的Object型属性Data,目前只需要一个键值对足够。另外JsonRequestBehavior = JsonRequestBehavior.AllowGet也举足轻重:为了避免JSON Hijacking攻击,自MVC2.0起任何以JsonResult类返回的请求都不允许Get型的Ajax请求获取数据,即便数据对象内才一个键值对,而妥协地采用Post请求也将使数据无法被浏览器缓存,其实这些数据的敏感度又不高,所以采用Get请求是个很好的开发体验!到目前为止,对客户端发来的Ajax请求,服务端在认证失败情况下给出了应答,即通知客户端脚本"我对你验证失败"----Ajax_UnAuthorized。
还记得Ajax.BeginForm()里的属性OnSuccess吧?现在看看它的JS函数:
客户端的接收也很干脆,如果通知信息两相统一,那么直接赋值给window.location作页面跳转,刷新整个浏览器。采用Request.Url获取到当前页面的路由网址作为跳转路径的参数一并携带过去,结果形如:
http://localhost:1650/Account/LogOn?returnUrl=http%3A%2F%2Flocalhost%3A1650%2F
强转到登录页时,得先让用户输入表单,待提交表单且登录验证通过后,才利用returnUrl参数值跳转回原来的页面,所以我们需要把returnUrl参数值暂存起来,看看LogOn.cshtml的登录表单头:
BeginForm()的第三位参数叫路由参数:object routeValues,我们把"返回页"暂存于此,待将来登录表单提交时,该参数将一并提交到服务器,交由标记了HttpPost特性的Action处理:
路由网址中的参数returnUrl可以在 HttpPost限定的Action作为参数获取:显然第一个是登录表单实体,第二个便是其参数。
只要判断传入参数returnUrl是否为空,便可作出恰当的跳转选择,所以只要returnUrl是一个具体的网址就可以顺利返回,这就是MVC3.0中对返回原页面的较为简洁而优雅的处理方案。