重复提交的场景很常见,可能是当时服务器延迟的原因,如购物车物品叠加,重复提交多个订单。常见的解决方法是提交后把Button在客户端Js禁用,或是用Js禁止后退键等。在ASP.NET MVC 3 Web Application中 如何去防止这类HTTP-Post的重复提交呢? 我们可以借助Session,放置一个Token在View/Page上,然后在Server端去验证是不是同一个Token来判断此次Http-Post是否有效。看下面的代码: 首先定义一个接口,便于扩展。
public interface IPageTokenView { /// <summary> /// Generates the page token. /// </summary> string GeneratePageToken(); /// <summary> /// Gets the get last page token from Form /// </summary> string GetLastPageToken { get; } /// <summary> /// Gets a value indicating whether [tokens match]. /// </summary> /// <value> /// <c>true</c> if [tokens match]; otherwise, <c>false</c>. /// </value> bool TokensMatch { get; } }
定义一个Abstract Class,包含一个
public abstract class PageTokenViewBase : IPageTokenView { public static readonly string HiddenTokenName = "hiddenToken"; public static readonly string SessionMyToken = "Token"; /// <summary> /// Generates the page token. /// </summary> /// <returns></returns> public abstract string GeneratePageToken(); /// <summary> /// Gets the get last page token from Form /// </summary> public abstract string GetLastPageToken { get; } /// <summary> /// Gets a value indicating whether [tokens match]. /// </summary> /// <value> /// <c>true</c> if [tokens match]; otherwise, <c>false</c>. /// </value> public abstract bool TokensMatch { get; } }
接着是实现SessionPageTokenView类型,记得需要在验证通过后生成新的Token,对于这个Class是把它放到Session中。
public class SessionPageTokenView : PageTokenViewBase { #region PageTokenViewBase /// <summary> /// Generates the page token. /// </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> /// Gets the get last page token from Form /// </summary> public override string GetLastPageToken { get { return HttpContext.Current.Request.Params[HiddenTokenName]; } } /// <summary> /// Gets a value indicating whether [tokens match]. /// </summary> /// <value> /// <c>true</c> if [tokens match]; otherwise, <c>false</c>. /// </value> public override bool TokensMatch { get { string formToken = GetLastPageToken; if (formToken != null) { if (formToken.Equals(GeneratePageToken())) { //Refresh token HttpContext.Current.Session[SessionMyToken] = GenerateHashToken(); return true; } } return false; } } #endregion #region Private Help Method /// <summary> /// Generates the hash token. /// </summary> /// <returns></returns> private string GenerateHashToken() { return Utility.Encrypt( HttpContext.Current.Session.SessionID + DateTime.Now.Ticks.ToString()); } #endregion
这里有到一个简单的加密方法,你可以实现自己的加密方法.
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 等机制
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public sealed class ValidateReHttpPostTokenAttribute : FilterAttribute, IAuthorizationFilter { public IPageTokenView PageTokenView { get; set; } /// <summary> /// Initializes a new instance of the <see cref="ValidateReHttpPostTokenAttribute"/> class. /// </summary> public ValidateReHttpPostTokenAttribute() { //It would be better use DI inject it. PageTokenView = new SessionPageTokenView(); } /// <summary> /// Called when authorization is required. /// </summary> /// <param name="filterContext">The filter context.</param> public void OnAuthorization(AuthorizationContext filterContext) { if (filterContext == null) { throw new ArgumentNullException("filterContext"); } if (!PageTokenView.TokensMatch) { //log... throw new Exception("Invaild Http Post!"); } } }
还需要一个HtmlHelper的扩展方法:
public static HtmlString GenerateVerficationToken(this HtmlHelper htmlhelper) { 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)); }
将输出这类的HtmlString:
<input name="hiddenToken" type="hidden" value="1AB01826F590A1829E65CBD23CCE8D53" />
我们创建一个叫_ViewToken.cshtml的Partial View,这样便于模块化,让我们轻易加入到具体View里,就两行代码,第一行是扩展方法NameSpace
@using Mvc3App.Models;
@Html.GenerateVerficationToken()
假设我们这里有一个简单的Login.cshtml,然后插入其中:
<form method="post" id="form1" action="@Url.Action("Index")"> <p> @Html.Partial("_ViewToken") UserName:<input type="text" id="fusername" name="fusername" /><br /> Password:<input type="password" id="fpassword" name="fpassword" /> <input type="submit" value="Sign-in" /> </p> </form>
这里我们Post的Index Action,看Controller代码,我们在Index上加上ValidateReHttpPostToken的attribute.
[HttpPost] [ValidateReHttpPostToken] public ActionResult Index(FormCollection formCollection) { return View(); } public ActionResult Login() { return View(); }
好的,完了,由于篇幅有限,单元测试代码不贴了。让我们运行程序在IE中. 正常点击Button后提交表单,此时按F5再次提交,看到这个提示框:
点击Retry后,这时就会出现预期Exception,这里只是为了演示,实际中可能需要记录日志,做异常处理。
Invaild Http Post!
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.Exception: Invaild Http Post!
有兴趣您可以自己试一下,希望对您Web开发有帮助。