重复提交的场景很常见,
1、可能是当时服务器延迟的原因,如购物车物品叠加,重复提交多个表单。
2、按F5 不断的刷新提交页地址,进行HttpPost
解决方法流程:
1、页面中生成一个加密的字符串,分别存在Session、Input隐藏域中
2、页面提交时,将Input隐藏域的值与Session的值进行比较,第一次提交两个值相等,不会抛出异常。
3、将Session存入随机数(刷新Session)
4、按F5刷新提交页地址,因为Session的值已经刷新,Input隐藏域的值没刷新,所有抛出异常"无效的HTTP POST!"
一、Models层
首先定义一个接口,便于扩展
namespace MvcApplication4.Models
{
public interface IPageTokenView
{
/// <summary>
/// 生成的页面标记
/// </summary>
string GeneratePageToken();
/// <summary>
/// 取得最后一页的标记形式
/// </summary>
string GetLastPageToken { get; }
/// <summary>
/// 获取一个值,指示是否匹配[标记]。
/// </summary>
/// <value>
/// <c>true</c> 如果[标记]不匹配<c>false</c>.
/// </value>
bool TokensMatch { get; }
}
}
定义一个Abstract Class定义一个抽象类,继承自接口
namespace MvcApplication4.Models
{
public abstract class PageTokenViewBase : IPageTokenView
{
public static readonly string HiddenTokenName = "hiddenToken";//隐藏域名字
public static readonly string SessionMyToken = "Token";//Session名
/// <summary>
/// 生成的页面标记
/// </summary>
/// <returns></returns>
public abstract string GeneratePageToken();
/// <summary>
/// 取得最后一页的标记形式
/// </summary>
public abstract string GetLastPageToken { get; }
/// <summary>
/// 获取一个值,指示是否匹配[标记]。
/// </summary>
/// <value>
/// <c>true</c> 如果[标记]不匹配 <c>false</c>.
/// </value>
public abstract bool TokensMatch { get; }
}
}
实例化抽象类的方法
namespace MvcApplication4.Models
{
public class SessionPageTokenView : PageTokenViewBase
{
#region PageTokenViewBase
/// <summary>
/// 生成的页面标记
/// </summary>
/// <returns></returns>
public override string GeneratePageToken()
{
if (HttpContext.Current.Session[SessionMyToken] != null)
{
return HttpContext.Current.Session[SessionMyToken].ToString();
}
else
{
var token = GenerateHashToken();
HttpContext.Current.Session[SessionMyToken] = token;
return token;
}
}
/// <summary>
/// 取得最后一页的标记形式
/// </summary>
public override string GetLastPageToken
{
get
{
return HttpContext.Current.Request.Params[HiddenTokenName];
}
}
/// <summary>
/// 获取一个值,指示是否匹配[标记]。
/// </summary>
/// <value>
/// <c>true</c> 如果[标记]不匹配 <c>false</c>.
/// </value>
public override bool TokensMatch
{
get
{
string formToken = GetLastPageToken;
if (formToken != null)
{
if (formToken.Equals(GeneratePageToken()))
{
//刷新Session["token"]
HttpContext.Current.Session[SessionMyToken] = GenerateHashToken();
return true;
}
}
return false;
}
}
#endregion
#region Private Help Method
/// <summary>
/// 生成哈希标记.
/// </summary>
/// <returns></returns>
private string GenerateHashToken()
{
return Utility.Encrypt(
HttpContext.Current.Session.SessionID + DateTime.Now.Ticks.ToString());
}
#endregion
}
}
这里有到一个简单的加密方法,你可以实现自己的加密方法.
namespace MvcApplication4.Models
{
public class Utility
{
/// <summary>
/// 加密
/// </summary>
/// <param name="plaintext">加密文本</param>
/// <returns></returns>
public static string Encrypt(string plaintext)
{
string cl1 = plaintext;
string pwd = string.Empty;
MD5 md5 = MD5.Create();
byte[] s = md5.ComputeHash(Encoding.Unicode.GetBytes(cl1));
for (int i = 0; i < s.Length; i++)
{
pwd = pwd + s[i].ToString("X");
}
return pwd;
}
}
}
我们再来编写一个Attribute继承FilterAttribute, 实现IAuthorizationFilter接口。然后比较Form中Token与Session中是否一致,不一致就Throw Exception. Tips:这里最好使用依赖注入IPageTokenView类型,增加Logging 等机制
namespace MvcApplication4.Models
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class ValidateReHttpPostTokenAttribute : FilterAttribute, IAuthorizationFilter
{
public IPageTokenView PageTokenView { get; set; }
/// <summary>
/// 新实例初始化 <see cref="ValidateReHttpPostTokenAttribute"/> class.
/// </summary>
public ValidateReHttpPostTokenAttribute()
{
PageTokenView = new SessionPageTokenView();//类实例化接口
}
/// <summary>
/// 在需要授权时调用.
/// </summary>
/// <param name="filterContext">筛选器上下文.</param>
public void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
//比较Form中Token与Session中是否一致,不一致就Throw Exception. Tips:这里最好使用依赖注入IPageTokenView类型,增加Logging 等机制
if (!PageTokenView.TokensMatch)
{
//log...
throw new Exception("无效的HTTP POST!");
}
}
}
}
还需要一个HtmlHelper的扩展方法:
namespace MvcApplication4.Models
{
/// <summary>
/// 拓展方法如何使用?
///@using MvcApplication4.Models; 1、引入拓展方法的命名空间
///@Html.GenerateVerficationToken() 2、调用拓展方法
/// </summary>
public static class GenerateVerficationTokenE
{
/// <summary>
/// htmlHelper拓展方法(将输出这类的HtmlString: <input name="hiddenToken" type="hidden" value="1AB01826F590A1829E65CBD23CCE8D53" />)
/// </summary>
/// <param name="htmlhelper"></param>
/// <returns></returns>
public static HtmlString GenerateVerficationToken(this HtmlHelper htmlhelper)
{
//Session
string formValue = Utility.Encrypt(HttpContext.Current.Session.SessionID + DateTime.Now.Ticks.ToString());
HttpContext.Current.Session[PageTokenViewBase.SessionMyToken] = formValue;
//向页面输出隐藏域.
string fieldName = PageTokenViewBase.HiddenTokenName;
TagBuilder builder = new TagBuilder("input");
builder.Attributes["type"] = "hidden";
builder.Attributes["name"] = fieldName;
builder.Attributes["value"] = formValue;
return new HtmlString(builder.ToString(TagRenderMode.SelfClosing));
}
}
}
二、Views层
@using MvcApplication4.Models;//1、引入拓展方法的命名空间
@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>编辑用户信息</h2>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
@using (Html.BeginForm())
{
@Html.ValidationSummary(true)
<div>
@Html.GenerateVerficationToken() //2、调用拓展方法
</div>
<div>
<input type="submit" value="保存" name="tj"/>
</div>
}
三、Controllers层
namespace MvcApplication4.Controllers
{
public class TestController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpPost]
[ValidateReHttpPostToken]//注意防止页面反复提交
public ActionResult Index(Person person)
{
string str = "";
if (ModelState.IsValid)//模型状态字典实例有效
{
//......
}
return Content(str);
}
}
}
让我们运行程序在IE中. 正常点击Button后提交表单后,在按F5刷新就会抛出异常
代码下载地址:http://download.csdn.net/detail/tiz198183/5148104