0

I have problem in when user post the data. Some times the post run so fast and this make problem in my website.

The user want to register a form about 100$ and have 120$ balance.

When the post (save) button pressed sometimes two post come to server very fast like:

2018-01-31 19:34:43.660 Register Form 5760$

2018-01-31 19:34:43.663 Register Form 5760$

Therefore my client balance become negative.

I use If in my code to check balance but the code run many fast and I think both if happen together and I missed them.

Therefore I made Lock Controll class to avoid concurrency per user but not work well.

I made global Action Filter to control the users this is my code:

public void OnActionExecuting(ActionExecutingContext context)
{
    try
    {
        var controller = (Controller)context.Controller;
        if (controller.User.Identity.IsAuthenticated)
        {
            bool jobDone = false;
            int delay = 0;
            int counter = 0;
            do
            {
                delay = LockControllers.IsRequested(controller.User.Identity.Name);
                if (delay == 0)
                {
                    LockControllers.AddUser(controller.User.Identity.Name);
                    jobDone = true;
                }
                else
                {
                    counter++;
                    System.Threading.Thread.Sleep(delay);
                }
                if (counter >= 10000)
                {
                    context.HttpContext.Response.StatusCode = 400;
                    jobDone = true;
                    context.Result = new ContentResult()
                    {
                        Content = "Attack Detected"
                    };
                }
            } while (!jobDone);

        }
    }
    catch (System.Exception)
    {
    }
}

public void OnActionExecuted(ActionExecutedContext context)
{
    try
    {
        var controller = (Controller)context.Controller;
        if (controller.User.Identity.IsAuthenticated)
        {
            LockControllers.RemoveUser(controller.User.Identity.Name);
        }
    }
    catch (System.Exception)
    {
    }   
}

I made list static list of user and sleep their thread until previous task happen.

Is there any better way to manage this problem?

Community
  • 1
  • 1
  • Why can't to disable the button after use click? You may still need to have logic at your server, but at least it would help you have a better use experience – Ankit Vijay Feb 04 '18 at 00:35
  • Man you should manage that issue on the frontend – NicoRiff Feb 04 '18 at 00:41
  • @Alexi I would disagree that it is a duplicate of the question asked. Since it's related to a web-api it could be a public facing API consumed by 3rd parties. Also your backend should define business rules not the front end. The question is more about data integrate that the fact the end point is being called twice. – Jonathan.Hickey Feb 04 '18 at 01:44
  • @alexei-levenkov: I disagree too with the closure. The linked question is about MVC not WebAPI-esque calls, and the accepted answer of redirecting doesn't work in here, but even then it wouldn't solve the issues of clicks that happen very fast (as shown on the timestaps in the OP). I've reopened it. The original linked to [this answer](https://stackoverflow.com/a/9803657/455493) as duplicate which isn't fitting imho – Tseng Feb 04 '18 at 11:31
  • @AnkitVijay this is not about front end. Maybe some one flood server by sending data and this not about front end – Fariba Eskandaribiroun Feb 04 '18 at 14:36
  • @NicoRiff this is web api not application to disable button only one user could login with 2 device and click on save together – Fariba Eskandaribiroun Feb 04 '18 at 14:37

2 Answers2

0

So the original question has been edited so this answer is invalid.

so the issue isn't that the code runs too fast. Fast is always good :) The issue is that the account is going into negative funds. If the client decides to post a form twice that is the clients fault. It maybe that you only want the client to pay only once which is an other problem.

So for the first problem, I would recommend a using transactions (https://en.wikipedia.org/wiki/Database_transaction) to lock your table. Which means that the add update/add a change (or set of changes) and you force other calls to that table to wait until those operations have been done. You can always begin your transaction and check that the account has the correct amount of funds.

If the case is that they are only ever meant to pay once then.. then have a separate table that records if the user has payed (again within a transaction), before processing the update/add.

http://www.entityframeworktutorial.net/entityframework6/transaction-in-entity-framework.aspx

(Edit: fixing link)

  • I could not lock table I need to lock user not table, There is 3000 user online and need this table and there is no reason another user wait for other user – Fariba Eskandaribiroun Feb 04 '18 at 14:40
  • Locking the table is for data integrity. The will only last a number of milliseconds. Its not like all 3000 are going to be updating the at the same time. And if they are, should write a test and see if your response time is too long. You are still able to read from the table if it is locked, using dirty reads. which wont slow down the rest of your application. When updating critical data like purchases and payments sometimes transnational locks are required. – Jonathan.Hickey Feb 04 '18 at 15:43
0

You have a few options here

  1. You implement ETag functionality in your app which you can use for optimistic concurrency. This works well, when you are working with records, i.e. you have a database with a data record, return that to the user and then the user changes it.

  2. You could add an required field with a guid to your view model which you pass to your app and add it to in memory cache and check it on each request.

    public class RegisterViewModel
    {
        [Required]
        public Guid Id { get; set; }
    
        /* other properties here */
        ...
    }
    

    and then use IMemoryCache or IDistributedMemoryCache (see ASP.NET Core Docs) to put this Id into the memory cache and validate it on request

    public Task<IActioNResult> Register(RegisterViewModel register)
    {
        if(!ModelState.IsValid)
            return BadRequest(ModelState);
    
        var userId = ...; /* get userId */
        if(_cache.TryGetValue($"Registration-{userId}", register.Id))
        {
            return BadRequest(new { ErrorMessage = "Command already recieved by this user" });
        }
    
        // Set cache options.
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Keep in cache for 5 minutes, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromMinutes(5));
    
        // when we're here, the command wasn't executed before, so we save the key in the cache
        _cache.Set($"Registration-{userId}", register.Id, cacheEntryOptions );
    
        // call your service here to process it
        registrationService.Register(...);
    }
    

    When the second request arrives, the value will already be in the (distributed) memory cache and the operation will fail.

    If the caller do not sets the Id, validation will fail.

Of course all that Jonathan Hickey listed in his answer below applies to, you should always validate that there is enough balance and use EF-Cores optimistic or pessimistic concurrency

Tseng
  • 61,549
  • 15
  • 193
  • 205
  • I see absolutely nothing WebAPI specific in your answer. It is very strange as you claim that prevention of double posts for [MVC](https://stackoverflow.com/questions/9803286/prevent-double-form-submissions) and WebAPI is completely different... Also I suspect OP would need way more information on how you suggest using ETag... And consider covering just regular replay attacks at least with some remark. – Alexei Levenkov Feb 04 '18 at 20:27
  • 2 (caching random request-ID to prevent replaying the same request) is somewhat weak as request can be replayed as soon as it is no longer cached (timeout, or in case of memory cache simply force restart by hitting some issue in the code/overload server). I'd expect one to to start with some versioning logic so requests can be easily compared by version to reject obsolete once. Doing the same with random numbers seem to be harder... (Plus version is frequently free with autoincremented fields in DB if you need to log the operation). – Alexei Levenkov Feb 04 '18 at 20:32
  • @AlexeiLevenkov: Right, the OP was about preventing two fast following requests to be processed so caching it for a few seconds or minutes is sufficient. Of course if you want to prevent it for eternally in far future, then one should persist it. But it wasn't a requirement. Also I didn't claim its different, just noted in the comments that the linked answer did not answer this question, since it was based purely on client (java script) code on the browser. Removed the WebAPI references though. Thanks for the suggestion – Tseng Feb 04 '18 at 20:38
  • I read the question's sample (some money sum manipulation) differently as replay of the same operation should be prevented, but maybe you are right and OP strictly wants to prevent instant replay. – Alexei Levenkov Feb 04 '18 at 21:10