重复提交的场景很常见,可能是当时服务器延迟的原因,如购物车物品叠加,重复提交多个订单。常见的解决方法是提交后把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();
- }