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(); }
彪悍的人生不需要解释,彪悍的代码不需要注释。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?