天天看點

Asp.net MVC中防止HttpPost重複送出

   重複送出的場景很常見,可能是當時伺服器延遲的原因,如購物車物品疊加,重複送出多個訂單。常見的解決方法是送出後把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再次送出,看到這個提示框:      
Asp.net MVC中防止HttpPost重複送出

點選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開發有幫助。 

原文出自:http://www.cnblogs.com/wintersun/archive/2012/03/10/2389084.html

繼續閱讀