ASP.NET Web Form和MVC中防止F5刷新引起的重复提交问题
转载 http://www.cnblogs.com/hiteddy/archive/2012/03/29/Prevent_Resubmit_When_Refresh_Reload_In_ASP_NET_Web_Form_MVC.html
什么是刷新/重新载入
IE中的刷新(Refresh),在FF和Chrome中称为重新载入(Reload),与正常进入页面的区别在于以下两点:
1. 缓存控制
如果文件(比如图片)在本地缓存中已经存在,正常进入页面会不访问服务器而直接从本地加载。而对于刷新操作,即使存在本地缓存,也会强制访问服务器检查更新,在Request Header中会带有“If-Modified-Since”标签。如果文件没有更新,服务器会返回304, 否则返回200及更新后的文件。
2. 重复提交
刷新会重复提交上一次的请求。比方说新建一张订单然后保存,而系统操作成功后停留在当前页面。此时进行刷新,浏览器会弹出一个对话框询问是否继续,点确定后浏览器会重复提交上一次的POST请求(点击保存的请求),系统可能会产生一张重复的订单。当然,上次操作如果只是Get请求而非POST,重复提交并不会导致这种问题。
如何防止重复提交
防止重复提交的方法与PRG(POST-Redirect-GET)模式有一些相似之处,具体来说,当Web服务器识别到一个重复提交的POST请求的时候,重定向到当前页面,然后浏览器以GET的方式请求该页面。如果使用HttpWatch之类的工具查看网络请求,可以看到会有两个请求,302 & 200。
现在问题在于如何识别一个请求是由刷新引起的。我们利用刷新操作会重复提交上一次请求,而不是当前请求的特性,可以有以下方案:在服务器端(Session)和页面(可以是页面上一个hidden field)上都设置一个整型标志位,在每次POST请求中都比较两者的值,然后自增。在正常提交情况下,浏览器会提交当前页面上hidden field的值,所以每次该值与Session中的都相等;在刷新情况下,提交的值是上次请求的值,就会与Session的不相等,由此得知是刷新操作引起的重复提交。
这类似于Struts中的Token验证。
在这里我们把页面hidden field称为Client Flag,具体的流程如下
什么是刷新/重新载入
IE中的刷新(Refresh),在FF和Chrome中称为重新载入(Reload),与正常进入页面的区别在于以下两点:
1. 缓存控制
如果文件(比如图片)在本地缓存中已经存在,正常进入页面会不访问服务器而直接从本地加载。而对于刷新操作,即使存在本地缓存,也会强制访问服务器检查更新,在Request Header中会带有“If-Modified-Since”标签。如果文件没有更新,服务器会返回304, 否则返回200及更新后的文件。
2. 重复提交
刷新会重复提交上一次的请求。比方说新建一张订单然后保存,而系统操作成功后停留在当前页面。此时进行刷新,浏览器会弹出一个对话框询问是否继续,点确定后浏览器会重复提交上一次的POST请求(点击保存的请求),系统可能会产生一张重复的订单。当然,上次操作如果只是Get请求而非POST,重复提交并不会导致这种问题。
如何防止重复提交
防止重复提交的方法与PRG(POST-Redirect-GET)模式有一些相似之处,具体来说,当Web服务器识别到一个重复提交的POST请求的时候,重定向到当前页面,然后浏览器以GET的方式请求该页面。如果使用HttpWatch之类的工具查看网络请求,可以看到会有两个请求,302 & 200。
现在问题在于如何识别一个请求是由刷新引起的。我们利用刷新操作会重复提交上一次请求,而不是当前请求的特性,可以有以下方案:在服务器端(Session)和页面(可以是页面上一个hidden field)上都设置一个整型标志位,在每次POST请求中都比较两者的值,然后自增。在正常提交情况下,浏览器会提交当前页面上hidden field的值,所以每次该值与Session中的都相等;在刷新情况下,提交的值是上次请求的值,就会与Session的不相等,由此得知是刷新操作引起的重复提交。
这类似于Struts中的Token验证。
在这里我们把页面hidden field称为Client Flag,具体的流程如下
在最后一步刷新页面的时候,hidden field的值为3,但是浏览器会重复提交上一次的请求(2),造成与Session中的值(3)不一致。
在ASP.NET MVC中的实现
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
添加一个自定义的ActionFilter.
public class NoResubmitAttribute : ActionFilterAttribute
{
private static readonly string HttpMehotdPost = "POST";
private static readonly string prefix = "postFlag";
private string nameWithRoute; public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controllerContext = filterContext.Controller.ControllerContext;
if (!controllerContext.IsChildAction)
{
var request = controllerContext.HttpContext.Request;
var session = controllerContext.HttpContext.Session;
nameWithRoute = generateNameWithRoute(controllerContext);
int sessionFlag = session[nameWithRoute] == null ? : (int)session[nameWithRoute];
int requestFlag = string.IsNullOrEmpty(request.Form[nameWithRoute]) ? : int.Parse(request.Form[nameWithRoute]);
// get or normal post: true;
bool isValid = !IsPost(filterContext) || sessionFlag == requestFlag;
if (sessionFlag == int.MaxValue)
{
sessionFlag = -;
}
session[nameWithRoute] = ++sessionFlag;
if (!isValid)
{
filterContext.Result = new RedirectResult(GenerateUrlWithTimeStamp(request.RawUrl));
return;
}
}
base.OnActionExecuting(filterContext);
} private string GenerateUrlWithTimeStamp(string url)
{
return string.Format("{0}{1}timeStamp={2}", url, url.Contains("?") ? "&" : "?", (DateTime.Now - DateTime.Parse("2010/01/01")).Ticks);
} private bool IsPost(ActionExecutingContext filterContext)
{
return filterContext.HttpContext.Request.HttpMethod == HttpMehotdPost;
} private string generateNameWithRoute(ControllerContext controllerContext)
{
StringBuilder sb = new StringBuilder(prefix);
foreach (object routeValue in controllerContext.RouteData.Values.Values)
{
sb.AppendFormat("_{0}", routeValue);
}
return sb.ToString();
} public override void OnResultExecuted(ResultExecutedContext filterContext)
{
base.OnResultExecuted(filterContext);
if (!filterContext.IsChildAction && !(filterContext.Result is RedirectResult))
{
//string format = "<script type='text/javascript'>$(function () [[ $('form').each(function()[[$('<input type=hidden id={0} name={0} value={1} />').appendTo($(this));]])]]); </script>";
string format = "<script type='text/javascript'> var forms = document.getElementsByTagName('form'); for(var i = 0; i<forms.length; i++)[[var ele = document.createElement('input'); ele.type='hidden'; ele.id=ele.name='{0}'; ele.value='{1}'; forms[i].appendChild(ele);]] </script>";
string script = string.Format(format, nameWithRoute, filterContext.HttpContext.Session[nameWithRoute]).Replace("[[", "{").Replace("]]", "}");
filterContext.HttpContext.Response.Write(script);
}
}
} 复制代码
为了提交易用性,这里将通过javascript创建HiddenField的部分也封装在ActionFilter里,当然你也可以在母板页中加此hiddenfield。
然后将此Attribute加在需要防止重复提交的Action之上,需要提醒的是,Get和Post的Action都需要加此Attribute,或者都不加。
[HttpGet]
[NoResubmit]
public ActionResult Index()
{
return View();
} [HttpPost]
[NoResubmit]
public ActionResult Index(int id)
{
return View();
} [NoResubmit]
public ActionResult About()
{
return View();
}
在ASP.NET Web Form中的实现
跟MVC一样,在Web Form中我们仍然可以使用加Hidden Field的方法,但是我更愿意使用ViewState(视图状态)来储存该标志位,这样就不许动态创建hidden field。
实现会放在一个继承自Web.UI.Page的BasePage类中,只需将页面类继承该BasePage即可。
public class BasePage : System.Web.UI.Page
{
private static readonly string prefix = "postFlag";
private string nameWithRoute; public BasePage()
{
nameWithRoute = generateNameWithRoute();
} protected override void LoadViewState(object savedState)
{
object[] allStates = (object[])savedState;
base.LoadViewState(allStates[]);
int requestFlag = string.IsNullOrEmpty(allStates[].ToString()) ? : int.Parse(allStates[].ToString());
int sessionFlag = Session[nameWithRoute] == null ? : (int)Session[nameWithRoute]; // get or normal post: true;
bool isValid = !IsPostBack || sessionFlag == requestFlag;
if (sessionFlag == int.MaxValue)
{
sessionFlag = -;
}
Session[nameWithRoute] = ++sessionFlag;
if (!isValid)
{
Response.Redirect(GenerateUrlWithTimeStamp(Request.RawUrl), false);
Response.End();
return;
}
} protected override object SaveViewState()
{
object[] allStates = new object[];
allStates[] = base.SaveViewState();
allStates[] = Session[nameWithRoute];
return allStates;
} private string generateNameWithRoute()
{
string fileSubfix = ".aspx";
return prefix + Request.FilePath.Replace(fileSubfix, "").Replace("/", "_");
} private string GenerateUrlWithTimeStamp(string url)
{
return string.Format("{0}{1}timeStamp={2}", url, url.Contains("?") ? "&" : "?", (DateTime.Now - DateTime.Parse("2010/01/01")).Ticks);
}
}
避免IE 8及之前版本的Bug
再此过程中发现IE8及之前IE版本中的一个Bug。在刷新操作中进行重定向到当前路径,IE9/Chrome/Firefox中会产生两个请求,302和200,但是在IE8中,在302后没有进行重定向,页面则变成空白页。
IE9/Chrome/Firefox: (正常)
IE8 and ealier: (异常)
并不清楚这是IE8的缺陷还是所谓by-designed的行为。为了避免这种情况,在上述两种实现中都在重定向的URL中加了一个时间戳,使之与原路径不一样来避免此问题。
该方案的限制性
由于我们使用Session来存放服务器端的标志位,所以当用户在同一浏览器中不同标签页打开同一个页面时会有些小问题。后面打开的标签页一切正常,但如果在上面做了一些操作后又回到前面打开的标签操作,会引起先打开的标签重定向。
ASP.NET Web Form和MVC中防止F5刷新引起的重复提交问题的更多相关文章
- 关于asp.net web form 和 asp.net mvc 的区别
asp.net web forms 有什么缺陷? 1.视图状态臃肿:服务器和客户端传输过程中包含了大量的试图状态——在现在的web程序中甚至多达几百kb,而且每次往返都会请求,导致服务器请求带宽增加, ...
- 添加asp.net mvc到现有的asp.net web form 应用程序
前言 asp.net mvc的前一版本为asp.net web Form(Asp.net mvc之前称为asp.net),其第一个版本与2002年年初发布.asp.net web form 属于.ne ...
- 在asp.net web form项目中添加webapi接口
我有一个支付宝服务网关是ASP.NET WEB FORM项目,但是最近这个网关需要对外提供几个接口,想了下,使用web api比较合适,实现很简单,GO 1,首先添加一个文件夹名字叫App_Start ...
- Asp.net web form url route使用总结
asp.net web form 使用URL路由 注不是mvc中的路由 一.前台控件使用路由,通过表达式生成url地址,注意给路由参数赋值,防止使用了其他路由表达式值方式1:<asp:Hyper ...
- ASP.Net Web Form<一> aspx文件编译及呈现
对比复习下JSP 1.jsp的本质是Servlet ,会在第一次被访问时会被翻译成一个类文件,从此对这个页面的访问都是由这个类文件执行后进行输出. aspx 本质是IHttpHandler 2.jsp ...
- ASP.NET Web API实践系列01,以ASP.NET Web Form方式寄宿
创建一个空的ASP.NET Web Form项目. 右键项目,添加新项,创建Web API控制器类,TestController. 删除掉TestController默认的内容,编写如下: using ...
- ASP.NET Web Froms开发模式中实现程序集的延迟加载
延迟加载是一个很大的诱惑,可以达到一些比较好的效果,比如: 1.在实体框架中,由于关联数据的数量和使用时机是不确定的,通过延迟加载,仅在使用的时候去执行关联数据的查询操作,减少无谓的数据查询操作,可以 ...
- ASP.NET(Web Form)绘制图表 -- Google Chart 三部曲
ASP.NET(Web Form)绘制图表 -- Google Chart 三部曲 ASP.NET(Web Form)绘制图表 -- Google Chart 三部曲 1. 网页绘制图表 Googl ...
- php中如何防止表单的重复提交
在php中如何防止表单的重复提交?其实也有几种解决方法. 下面小编就为大家介绍一下吧.需要的朋友可以过来参考下 代码: <?php /* * php中如何防止表单的重复提交 * by www.j ...
随机推荐
- 面试题_66_to_75_Java IO 和 NIO 的面试题
IO 是 Java 面试中一个非常重要的点.你应该很好掌握 Java IO,NIO,NIO2 以及与操作系统,磁盘 IO 相关的基础知识.下面是 Java IO 中经常问的问题. 66)在我 Java ...
- Google发布SSLv3漏洞简要分析报告
今天上午,Google发布了一份关于SSLv3漏洞的简要分析报告.根据Google的说法,该漏洞贯穿于所有的SSLv3版本中,利用该漏洞,黑客可以通过中间人攻击等类似的方式(只要劫持到的数据加密两端均 ...
- BZOJ3681: Arietta
题解: 数据结构来优化网络流,貌似都是用一段区间来表示一个点,然后各种乱搞... 发现主席树好吊...在树上建主席树貌似有三种方法: 1.建每个点到根节点这条链上的主席树,可以回答和两点间的路径的XX ...
- Oracle 数据库整理表碎片
Oracle 数据库整理表碎片 转载:http://kyle.xlau.org/posts/table-fragmentation.html 表碎片的来源 当针对一个表的删除操作很多时,表会产生大量碎 ...
- [Sciter系列] MFC下的Sciter–5.Sciter中GUI线程研究
[Sciter系列] MFC下的Sciter–5.Sciter中GUI线程研究,目前MFC存在问题,win32没问题. 本系列文章的目的就是一步步构建出一个功能可用,接口基本完善的基于MFC框架的Sc ...
- B树索引和位图索引的区别!
B树索引主键和唯一性约束字段的B树索引,效率几乎和海量数据没有关系. 键值重复率低的字段比较适合使用B树索引. 位图索引键值重复率高的字段比较适合使用位图索引.count.and.or.in这些特定的 ...
- redis错误汇总
1.redis因为内存不够而启动失败 Microsoft Open Tech group 在 GitHub上开发了一个REDIS Win64的版本,项目地址是:https://github.com/M ...
- 翻译【ElasticSearch Server】第一章:开始使用ElasticSearch集群(4)
停止ElasticSearch(Shutting down ElasticSearch) 尽管我们期望集群(或节点)终生完美运行,我们最终可能需要重启或者正确的停止它(例如,维护).有三种方式来停止E ...
- 根据给定的日期给 dateEdit 控件增加颜色
private void dateEdit1_DrawItem(object sender, DevExpress.XtraEditors.Calendar.CustomDrawDayNumberCe ...
- JQuery WEB前段开发
JQuery WEB前段开发 Jquery是继prototype之后又一个优秀的Javascript框架.它是轻量级的js库 ,它兼容CSS3,还兼容各种浏览器(IE 6.0+, FF 1.5+, S ...