仕事が派手にドッタンバッタンしたので更新が途切れましたなー。
WebでSubmitボタンを連打された時の防止策についてメモしておきます。
まず、この記載での開発環境は、ざっくり
- Visual Studio2013update4
- .NET Framework4.5
- ASP.NET MVC5
です。
制御は、ActionFilterだけでよいといえばよい気もしますが、個人的には、クライアント側でもjavascriptで制御してます。この実装についてはここでは触れませんが、ここらへんを参考にしてやってます。
http://technoesis.net/prevent-double-form-submission-using-jquery/technoesis.net
Reference
さて、本題ですが、ActionFilterの実装は、この方のサイトを参考にしています。
rion.io
まず、ActionFilterの基本については、こちらのサイトを参照くらいでしょうか。
https://msdn.microsoft.com/ja-jp/library/dd381609(v=vs.100).aspx
ざっくりまとめると、
Overview
やりたいことは、Submitボタンを押された後、処理が終わる前にsubmitを連打されれいたら、検知して、無視するとかエラー返すとかです。
ActionFilterのOnActionExecutingメソッドで、以下のことを実装して実現します。
- Requestがあったユーザーを特定する仕組みをつくる
- ユーザーが最初のアクセス(=submit)から特定秒数の間にアクセスがあったら、検知して処理(エラーとか無視とか)を行う
Implimentation : 1
まず、検証するControllerとView作っちゃいます。
Controllerはこんな感じでざっくり。
public class DemoController : Controller { public ActionResult Index() { return View(); } [HttpPost] public async Task<ActionResult> DemoSubmit() { await Task.Delay(3000); TempData["SummittedTime"] = DateTime.Now.ToString("hh:MM:ss fff"); return RedirectToAction("Index"); } }
submitしたら、3秒待って、Viewを表示するだけです。ただ、submitした時間を表示します。
今回の話題とは全く関係ない小ネタですが、理由なくRedirectToActionさせて時間を表示するのでViewBagではなくTempDataに時間を入れて、表示できるようにしています。
ViewBagに入れるとRedirectしたらデータ消えちゃいますよね...。
Viewは、Index.cshtmlをこんな感じでまったり作りました。
<h2>demo prevent doubl submittion</h2> @if (TempData["SummittedTime"] != null) { <p>@TempData["SummittedTime"] </p> } @using (Html.BeginForm("DemoSubmit", "Demo")) { <input class="btn btn-warning" type="submit" name="DemoSubmit" value="3秒以内の連打禁止だおー" /> }
Implimentation : 2
では、本題のActionFilterです。
Propertyをいくつか用意してます。
まず、「DelayTimer」。同一ユーザーから一度Requestを受けたら、delayする時間を設定するプロパティ。秒で設定します。今回はデフォルトで3秒を設定しました。
後は、エラーメッセージだったり、RedirectするUrl(今回は全然触れてない...)。
using System; using System.Diagnostics; using System.Linq; using System.Net; using System.Security.Cryptography; using System.Text; using System.Web.Caching; using System.Web.Mvc; public class PreventDoubleSubmitAttribute : ActionFilterAttribute { #region class header private int _delayTimer = 3; public int DelayTimer { get { return _delayTimer; } set { _delayTimer = value; } } private string _err = "DoubleSubmit occurred"; public string ErrorMessage { get { return _err; } set { _err = value; } } public string RedirectUrl { get; set; } #endregion }
では、コアとなるOnActionExecutingの処理です。
まず、ユーザーを一意に認識させるために、リクエストの中の"HTTP_X_FORWARDED_FOR"を取得します。nullだったら、UserHostAddressを取得します。
その情報に、リクエストのUserAgentを追加しています。
あと、リスエストのUrlとパラメータを取得します。
それらの情報からMD5ハッシュを生成して、ユーザーの一意となるキーを作ります。
そして、ハッシュがCacheに存在するかを確認します。
存在する場合は、Prevent対象と判断し、なんらかの処理を入れます。今回は、BadRequestを返してしまっています。
存在しなければ、初回のリクエストと判断し、Cacheに値をセットします。その際に、そのCacheの有効期限にDelayTimerプロパティの秒数をセットします。
public override void OnActionExecuting(ActionExecutingContext filterContext) { var request = filterContext.HttpContext.Request; var cache = filterContext.HttpContext.Cache; //一意となるデータを取得 var firstrequest = request.ServerVariables["HTTP_X_FORWARDED_FOR"] ?? request.UserHostAddress; firstrequest += request.UserAgent; var current = request.RawUrl + request.QueryString; //ハッシュ生成 var hash = string.Join("", MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(firstrequest + current)).Select(s => s.ToString("x2"))); if (cache[hash] != null) { //今回は、BadRequestをなげます。 filterContext.Result = new HttpStatusCodeResult(HttpStatusCode.BadRequest); base.OnActionExecuting(filterContext); //おもむろにログを出してみたり...。 var controller = filterContext.RouteData.Values["controller"].ToString(); var action = filterContext.RouteData.Values["action"].ToString(); Trace.TraceWarning("Double Submit Occured: {0} - {1}", controller, action); } else { //存在しなければ、キャッシュに値をセットする cache.Add(hash, string.Empty, null, DateTime.Now.AddSeconds(DelayTimer), Cache.NoSlidingExpiration, CacheItemPriority.Default, null); } base.OnActionExecuting(filterContext); }
Implimentation : 3
最後にControllerにAttributeを追加してあげれば完了です。
[HttpPost]
[PreventDoubleSubmit]
public async Task<ActionResult> DemoSubmit()
{
....
これで、一度Submitした後、3秒以内に連打するとBadRequestが出力されます。
DelayTimerの時間など、プロパティにアクセスしたい場合は、こんな感じで書いてあげればよいです。
[HttpPost] [PreventDoubleSubmit(DelayTimer = 5)] public async Task<ActionResult> DemoSubmit()
今回は、javascriptで制御しているのに、それを掻い潜って連打攻撃してきたのであればBadRequestで沈める想定なのですが、ModelStateにエラーメッセージを追加したいとかもあるでしょうか。
if (cache[hash] != null) { filterContext.Controller.ViewData.ModelState.AddModelError("DoubleSubmit", ErrorMessage); } else { ...
こうした場合、controller側でModelstateのエラーを拾ってあげればよいです。
if (ModelState.Values.Select(vals => ModelState["DoubleSubmit"]).Any(v => v != null)) { //なんかエラー処理を... }
なんか色々ざっくり書いてしまったメモですが、今回はこの辺で...。