重復(fù)提交的場景很常見,可能是當(dāng)時服務(wù)器延遲的原因,如購物車物品疊加,重復(fù)提交多個訂單。常見的解決方法是提交后把Button在客戶端Js禁用,或是用Js禁止后退鍵等。在ASP.NET MVC 3 Web Application中 如何去防止這類HTTP-Post的重復(fù)提交呢? 我們可以借助Session,放置一個Token在View/Page上,然后在Server端去驗(yàn)證是不是同一個Token來判斷此次Http-Post是否有效??聪旅娴拇a: 首先定義一個接口,便于擴(kuò)展。 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; } }
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; } }
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 這里有到一個簡單的加密方法,你可以實(shí)現(xiàn)自己的加密方法. 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, 實(shí)現(xiàn)IAuthorizationFilter接口。然后比較Form中Token與Session中是否一致,不一致就Throw Exception. Tips:這里最好使用依賴注入IPageTokenView類型,增加Logging 等機(jī)制 [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!"); } } }
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)); }
<input name="hiddenToken" type="hidden" value="1AB01826F590A1829E65CBD23CCE8D53" /> 我們創(chuàng)建一個叫_ViewToken.cshtml的Partial View,這樣便于模塊化,讓我們輕易加入到具體View里,就兩行代碼,第一行是擴(kuò)展方法NameSpace
@using Mvc3App.Models; @Html.GenerateVerficationToken() 假設(shè)我們這里有一個簡單的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(); } 好的,完了,由于篇幅有限,單元測試代碼不貼了。讓我們運(yùn)行程序在IE中. 正常點(diǎn)擊Button后提交表單,此時按F5再次提交,看到這個提示框: ![]() 點(diǎn)擊Retry后,這時就會出現(xiàn)預(yù)期Exception,這里只是為了演示,實(shí)際中可能需要記錄日志,做異常處理。 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開發(fā)有幫助。 (責(zé)任編輯:管理員) |
|