(转)ASP.NET MVC 3.0:基于Ajax的表单提交,A页面认证失败后页面被强转至登录页面,待登录成功将如何回到A页面?

原文地址:http://www.cnblogs.com/luoxiaonet/archive/2011/12/13/2285326.html#commentform

 

    很多网站的首页都提供信息的输入,而不论您是否有账户且已登录。比如我喜欢逛的42qu(我跟创始人无任何关系,仅是喜欢该网站且无意广告,有兴趣的可以瞧瞧去)。当我发表自己的"碎碎念"时,会被自动跳转到登录页,而问题是登录成功后能否再回到原来的页面。听起来这个问题略显乏善可陈,然而它的实现框架是MVC 3.0,而且为了寻求其优雅的实现方式尝试了很多天,现作为记要并分享一下。

    先来一张最终效果图吧:

image

 

上图的页面切分为两个区域,一是碎碎念的发表区(上部分)、另一个是呈现区(下部分),以真分页提取数据;信息提交是基于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中对返回原页面的较为简洁而优雅的处理方案。



分类: ASP.NET
« 博主前一篇:ASP.NET MVC 3.0:Razor视图引擎如何展示多实体


posted @ 2011-12-13 19:44 不觉流年似水 阅读(1020) 评论(8)编辑 收藏

 回复 引用 查看  
#1楼 2011-12-13 21:08 Chairo     
42qu不是python做的?怎么变成.net mvc3.0了?
 回复 引用 查看  
#2楼[楼主] 2011-12-14 07:23 不觉流年似水     
引用Chairo:42qu不是python做的?怎么变成.net mvc3.0了?

嗯,是Python.
MVC3是我自己玩的方式。

returnUrl = Request.Url,如果在post的时候验证登录失效,这个时候,取出的Request.Url的地址是这个post的路径地址,登录后跳转这个地址显然不对,这种情况怎么处理?
代码看上去很规范,排版也很不错,顶一下。
继续好好写博客。

 回复 引用 查看  
#5楼 2011-12-14 10:22 V.Enjoy     
非常好!辛苦了!多谢分享!
 回复 引用 查看  
#6楼[楼主] 2011-12-14 10:44 不觉流年似水     
引用黑子范:returnUrl = Request.Url,如果在post的时候验证登录失效,这个时候,取出的Request.Url的地址是这个post的路径地址,登录后跳转这个地址显然不对,这种情况怎么处理?

引用黑子范:returnUrl = Request.Url,如果在post的时候验证登录失效,这个时候,取出的Request.Url的地址是这个post的路径地址,登录后跳转这个地址显然不对,这种情况怎么处理?

谢谢您的提问。
比如现在有一个功能页面A,它的路由网址是:http://localhost:1066/A
在文本框输入一些文本,预期我们会把文本信息异步提交给Controller层的Action处理(取名叫CreateChat),但实际上Action执行前还有一个拦截过滤器(自定义名称是LoginAuthorize),当它发现你没有登录或没有相应权限时,会针对你的异步请求返回一个JSON对象,并将控制权迅速移交给客户端(这样显式屏蔽了服务器抛401异常),意思便是“您认证失败,请看着办吧”,客户端脚本获得控制权后不会无动于衷的,它立马在回调函数中判断JSON的键值如果是"Ajax_UnAuthorized",便意识到“喔,我错了,我应该登录一下”,于是A页面脚本中的Request.Url获取到自己的路由网址(http://localhost:1066/A),作为网址参数,借助window.location的浏览器重定向至LogOn页面,所以“跳转这个地址显然不对”这种情况是不会发生的。
    接下来,在登录页中,如果登录验证不通过(账号不存在或密码不正确),你将一直停留在登录页;如果验证成功,即 LogOn这个Actoin的参数returnUrl不为空时,将由“return Redirect(returnUrl)”前往A页面。
可以建个MVC项目玩一下,呵呵,有疑问再沟通。

引用不觉流年似水:
引用黑子范:returnUrl = Request.Url,如果在post的时候验证登录失效,这个时候,取出的Request.Url的地址是这个post的路径地址,登录后跳转这个地址显然不对,这种情况怎么处理?

引用黑子范:returnUrl = Request.Url,如果在post的时候验证登录失效,这个时候,取出的Request.Url的地址是这个post的路径地址,登录后跳转这个地址显然不对,这种情况怎么处理?

谢谢您的提问。
比如现在有一个功能页面A,它的路由网址是:http://localhost:1066/A
在...

懂了,谢谢你的回答,可是如果不是ajax的提交呢,returnUrl在什么地方取?拦截器LoginAuthorize(显然不对)?如果是在提交前就获取一下再post,这样又挺繁琐,有木有好的办法

 回复 引用 查看  
#8楼[楼主] 2011-12-14 13:48 不觉流年似水     
引用黑子范:


如果不是Ajax请求,同时以后还希望回到该页面,则意味着在发表文本信息时,就得把A页面的路由网址作为参数一并提交给预期执行的Action(名叫CreateChat),当自定义过滤器LoginAuthorize拦截时,发现阁下没有登录,于是处理手段分两情况:
1.也就是本文的处理情况。询问客户端的请求是不是Ajax请求,在自定义过滤器LoginAuthorize中有IsAjaxReuquest()的判断,如果是就立即把控制权移交给客户端;
2.如果不是Ajax请求,即客户端的表单是形同@Html.BeginForm(),那么表单头只需要多加一个传递参数:Request.Url,完整写法大概是这样:
@using(Html.BeginForm("CreateChat", "Chat", new{ returnUrl = Request.Url }, FormMethod.Post))
{
//提示:该代码未测试
}
可以发现,Request.Url是第一次提交时就主动发给服务器,而不像Ajax请求那样要等到服务器下达一个通知后以window.location强行跳转再以参数携入。
这便是你希望的“在提交前就获取一下再post”的愿望(即改成Html表单后,多填入一个路由参数)。

Request.Url传到服务器,当我们默认不作任何处理,即试图以Authorize[]作为登录验证时,一旦验证失败MVC框架会抛出401异常,进而根据Web.config中的配置信息(正文有截图)重定向到登录页,且不带任何参数,而不带参数,又如何将returnUrl传递出去呢,问题就在这里!所以在自定义过滤器LoginAuthorize运行阶段,MVC框架认证失败且判定不是Ajax请求情况下,可以显式重定向到登录页,并将这个Url携带过去:
return RedirectToAction("LogOn", "Account", new { returnUrl = Request.QueryString["returnUrl"].ToString });
登录页LogOn代码和原来一样,不用更改,即LogOn根本不关心你原来是否是Ajax。

posted on 2011-12-15 22:44  黑子范  阅读(2535)  评论(0编辑  收藏  举报

导航