Click here to Skip to main content
15,881,281 members
Articles / Web Development / ASP.NET

Throttle Requests to a .NET MVC Action with a Custom Action Filter

Rate me:
Please Sign up or sign in to vote.
4.50/5 (2 votes)
28 Dec 2012CPOL3 min read 13.7K   7   3
In this programming article, I will show you how to create a custom action filter for .NET MVC which will throttle repeat requests.

Overview

In my day job, I work for HP Enterprise Security Services, part of my role is building secure and robust web applications which do everything possible to prevent malicious attacks. One of the most simple things you can do in your MVC project is to prevent repeat requests to a page. This is primarily used in form submissions, for example in the comments box you see on Jambr, I don't want people to be able to repeatedly post to it over and over again, I want to introduce a time limit in-between these requests. Also, there are going to be a lot of places on a typical site you want to limit such behavior, but don't want to repeat the code everywhere. This is where custom Action Filter Attributes come in.

Creating an Attribute

Creating a custom attribute is easy, take a look at this piece of code, I'll explain what it does below:

VB.NET
<AttributeUsage(AttributeTargets.Method, AllowMultiple:=False)>
Public NotInheritable Class RequestThrottleAttribute
    Inherits ActionFilterAttribute

    Public Overrides Sub OnActionExecuting(filterContext As ActionExecutingContext)
        'Do some logic in here to decide what is going to happen
    End Sub
End Class

What we're doing here is inheriting from the ActionFilterAttribute, class and overriding the OnActionExecuting method, which is where we will put our logic. I have decorated this class with some attributes of their own, AttributeTargets.Method states that this attribute can only be used on methods, and AllowMultiple states that there can only be one instance of it.

Expanding on the Base Attribute

The next thing to do is expand our logic out a bit. Let's make this attribute as flexible as possible, so for example, let's make the amount of time between requests flexible, give the option to either Redirect when an error occurs or simply add an error to the ModelState dictionary. Start by adding some properties to represent our customizable options:

VB.NET
''' <summary>
''' The amount of time between each request
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property TimeBetweenRequests As Integer = 5

''' <summary>
''' The name of the object in the ModelState to add an error too
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property ModelErrorName As String = Nothing

''' <summary>
''' The message to add to the ModelState object specified in ModelErrorName
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property ModelErrorValue As String = "Maximum number of requests exceeded"

''' <summary>
''' A URL to redirect to
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property RedirectOnError As String = Nothing

So in order to add an error to the ModelState dictionary, we need the name of the object to associate the error to (of course, this could just be an empty string for a generic error) - this is passed as ModelErrorName, and we also need the message to set - this is passed in ModelErrorValue. If we wanted to redirect instead, we would set RedirectOnError.

Caching and Cache Expiration

Next, we need to customize the OnActionExecuting method to do our throttling. We need to store somewhere the fact that a given user (let's define a user by their IP address as well as their user agent) has been to the page recently. I decided to generate a unique key from the information given, and store it in the HttpContext.Cache and set it to expire on a time which is equal to the TimeBetweenRequests parameter of our attribute. That way on the next request, all we need to do is check for the existence of the same key in the cache. Take a look at the code below of the OnActionExecuting method:

VB.NET
    Public Overrides Sub OnActionExecuting(filterContext As ActionExecutingContext)

        Dim HttpContext = filterContext.HttpContext

        'Get the details of the path they're requesting
        Dim pathInfo = HttpContext.Request.ServerVariables("PATH_INFO") & _
        filterContext.HttpContext.Request.ServerVariables("QUERY_STRING")
        
        'Get who requested it, get their user agent as well, 
        'as multiple people in the same room could be coming from the same IP
        Dim requestedBy = HttpContext.Request.ServerVariables("REMOTE_ADDR") & _
        HttpContext.Request.ServerVariables("HTTP_USER_AGENT")

        'Generate a unique key based on it
        Dim key = MD5(pathInfo & requestedBy)

        'Check to see if that key is in the cache
        If HttpContext.Cache.Get(key) IsNot Nothing Then
            'Reject the request
            If ModelErrorName IsNot Nothing Then
                'Add it to the modelstate
                filterContext.Controller.ViewData.ModelState.AddModelError_
                                           (ModelErrorName, ModelErrorValue)
            End If
            If RedirectOnError IsNot Nothing Then
                'Redirect
                filterContext.Result = New RedirectResult(RedirectOnError, False)
            End If
        Else
            'Add it to the cache
            HttpContext.Cache.Add(key, New Object, Nothing, _
            Now.AddSeconds(TimeBetweenRequests), Cache.NoSlidingExpiration, _
                                             CacheItemPriority.Normal, Nothing)
        End If

    End Sub

In case you don't already have code to create an MD5 of a string, here it is, you'll need to import System.Security.Cryptography:

VB.NET
Public Shared Function MD5(ByVal strToHash) As String
    Dim bytToHash As Byte() = ASCIIEncoding.ASCII.GetBytes(strToHash)
    Dim tmpHash As Byte() = (New MD5CryptoServiceProvider).ComputeHash(bytToHash)
    Dim i As Integer
    Dim sOutput As New StringBuilder(tmpHash.Length)
    For i = 0 To tmpHash.Length - 1
        sOutput.Append(tmpHash(i).ToString("X2"))
    Next
    Return sOutput.ToString()
End Function

Using the New Attribute

Now that your attribute is complete, all you need to do is implement it by decorating a given method with it. Take these two examples:

VB.NET
<HttpGet>
<RequestThrottle(TimeBetweenRequests:=10, RedirectOnError:="/Error/Throttle")>
Function TestThrottle()
    Return View(New TestThrottleViewModel)
End Function

This action method will only allow one request per 10 seconds to the url /TestThrottle, before it redirects to an error page. The next example will add the error to the ModelState instead, so you can return to the user on the same page:

VB.NET
<HttpPost>
<RequestThrottle(TimeBetweenRequests:=10, ModelErrorName:="Comment", _
ModelErrorValue:="There is a 10 second wait between posts")>
Function TestThrottle(ByVal model As TestThrottleViewModel)
    If ModelState.IsValid Then
        Return Content("Thanks")
    Else
        Return View(model)
    End If
End Function

The simple view model that I have created for the postback contains just one item, "Comment", from there I've created a form using the .NET HTML Helpers:

VB.NET
<% Using Html.BeginForm() %>
    <%: Html.ValidationSummary(True) %>

    <fieldset>
        <legend>TestThrottleViewModel</legend>

        <div class="editor-label">
            <%: Html.LabelFor(Function(model) model.Comment) %>
        </div>
        <div class="editor-field">
            <%: Html.EditorFor(Function(model) model.Comment) %>
            <%: Html.ValidationMessageFor(Function(model) model.Comment) %>
        </div>

        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
<% End Using %>

The first postback works fine, the second will result in the user being greeted with the error:

Image 1

Conclusion

I hope this simple tutorial has helped you to think a little about your site security, as well as how to utilize custom Action Filters to reuse code across your website. As always, any questions, please ask.

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Hewlett Packard Enterprise Security Services
United Kingdom United Kingdom
Technical Architect for Hewlett-Packard Enterprise Security Service.

Please take the time to visit my site

Comments and Discussions

 
QuestionWill this work across web farms ? Pin
Robert Slaney24-Jan-13 10:26
Robert Slaney24-Jan-13 10:26 
AnswerRe: Will this work across web farms ? Pin
Karl Stoney31-Jan-13 1:44
Karl Stoney31-Jan-13 1:44 
QuestionGood accidental double post protection Pin
Mike Lang7-Jan-13 5:05
Mike Lang7-Jan-13 5:05 
Very useful, thanks.

For single submission forms I just disable the button via javascript. But for comment forms and others where you need to be able to enter more than one item, this looks good.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.