ASP.NET с отключенной кнопкой с дублирующими постбэками - PullRequest
1 голос
/ 10 февраля 2012

У меня есть страница asp.net, на которой можно ввести информацию о кредитной карте и сумму платежа для авторизации платежа. Внезапно, около 2 недель назад, мы начали получать отчеты о двойных платежах, но мы не внесли никаких изменений в страницу. Страница уже настроена на отключение кнопки отправки при нажатии. Пытаясь решить проблему, с тех пор я также установил флаг на странице при нажатии кнопки, чтобы, если этот флаг установлен, он не позволил бы кнопке выполнить обратную передачу (этот метод мы используем на другой странице). это не имеет проблем), но это продолжает происходить.

Есть несколько причин, по которым я считаю пользователя, обновляющего страницу, крайне маловероятным источником проблемы. Во-первых, мы отображаем страницу в элементе управления веб-браузера WPF, она соответствует окну, в котором оно находится, и единственным признаком того, что это даже веб-страница, является шум щелчка постбэков, если вы щелкнули правой кнопкой мыши по телу, или если ошибка страницы. Единственные кнопки обновления или возврата находятся в контекстном меню браузера. Далее, я не могу думать о том, что у пользователей может быть желание обновить или вернуться, пока они не получат сообщение об ошибке на странице, но они не сообщают об ошибках в процессе. Наконец, я принял меры, чтобы избежать дублирования обратных передач на стороне сервера, поместив токен в сеанс и проверив его перед обработкой карты. Таким образом, пользователю придется обновить и нажать кнопку «Повторить» быстрее, чем первый запрос может записать токен в состояние сеанса. Самый быстрый способ добиться этого - нажать ВВОД, F5, Ввести все подряд. Я ненавижу игнорировать единственный способ, которым я знаю, что это могло произойти, но кажется безопасным сказать, что это не то, что происходит. Наконец, после публикации страницы, приложение WPF сообщает через приложение сценариев, что оно может закрыться, чтобы пользователь не смог ничего сделать на странице после обратной передачи до исчезновения браузера.

Единственная проблема в том, что я не знаю, что происходит. Каким-то образом представление только что прошло охрану javascript и защиту сервера токенов на стороне сервера и получило двойной заряд, и я понятия не имею, как. Они были зарегистрированы как происходящие в течение 2 секунд друг от друга. Я убедился, что код нашего приложения WPF не вызывает «Обновить» и не контролирует навигацию браузера. У кого-нибудь есть идеи?

UPDATE Вот некоторые из соответствующих кодов:

    <style type="text/css">
        ...
    </style>

    <script type="text/javascript" language="javascript">
        function OnProcessing(button) //
        {
            //Check if client side validation passes before disabling

            // if postback - return false. If it's 1, then it's a postback.
            if (document.getElementById("<%=HFSubmitForm.ClientID %>").value == '1') {
                return false;
            }
            else {
                // mark that submit is to be done and return true
                document.getElementById("<%=HFSubmitForm.ClientID %>").value = '1';
                button.disabled = true;
                window.external.OnPaymentProcessing();
                return true;
            }
        }

    </script>
</head>
<body id="body" runat="server" style="font-family: arial, Helvetica, sans-serif; font-size: 11px;" scroll="no" onkeydown="return CancelEnterKey(event)">
    <form id="form1" runat="server">
        <asp:scriptmanager ID="Scriptmanager1" runat="server" EnablePageMethods="True"></asp:scriptmanager>
        <script src="Resources/Scripts/CardInput.js?<%= DateTime.Now.Ticks %>" type="text/javascript" language="javascript"></script>

        <div id="divCardSwiper" style="text-align:center;" runat="server">
            <input id="txtSwipeTarget" type="text" onblur="FocusOnSwipeTarget()" onkeydown="return SwipeTargetCharAdded(event)"
                    style="position: absolute; left: -1000px" />
            <table style="margin-left:auto; margin-right:auto">
                <tr>
                    <td style="text-align:center">
                        <span style="font-size: 20pt; font-weight: bold; color: #808080">Please Swipe Credit Card</span>
                    </td>
                </tr>
                <tr><td style="text-align:center"><img alt="Card Swiper Image" src="Resources/scra-magnesafe-mini-3.png"/></td></tr>
                <tr><td style="text-align:center"><span style="font-size: 12pt; font-weight: bold; color: #808080">Or <a href="#" onclick="ManualEntry();return false;">click here</a> to enter manually.</span></td></tr>
            </table>
        </div>
        <div id="divCcForm" runat="server">
            <table>
                <!-- Input Fields -->
            </table>
            <asp:Label ID="lblError" runat="server" Font-Bold="True"  ForeColor="Red"></asp:Label>
            <div style="text-align:center;">
                <asp:Button ID="btnProcess" runat="server"
                Text="Process" OnClick="btnProcess_Click" OnClientClick="if (OnProcessing(this)==false){return false;}" UseSubmitBehavior="False"/>
                <p><strong>Processing may take a moment.<br><font color="red">PLEASE ONLY CLICK PROCESS ONCE</font></strong></p>
            </div>

        </div>
        <asp:Label ID="label1" runat="server" Visible="False"></asp:Label>
        <asp:HiddenField ID="HFRequestToken" runat="server"/>
        <asp:HiddenField ID="HFSubmitForm" runat="server"/>
    </form>
</body>

    protected void btnProcess_Click(object sender, EventArgs e)
    {
        if (IsProcessing())
        {
            //Payment was already processing
            btnProcess.Enabled = false; //Make sure button doesn't become available again
            logger.Warn(String.Format("PaymentCollection.aspx was submitted multiple times. Only processing the initial request (Session Token: {0}). FacilityID: {1}, FamilyID: {2}, Amount: {3}",
                                                Session[_postBackTokenKey], ViewState[_facilityIDKey], ViewState[_familyIDKey], txtAmount.Text));
            return;
        }

        lblError.Text = String.Empty;
        string script = "window.external.OnPaymentProcessingCancelled()";
        bool isRefund = (bool)ViewState[_isRefundKey];
        bool processed = false;

        if (ValidateForm(isRefund))
        {
            ProcessingInput pi = new ProcessingInput();

            try
            {
                CreditCardType cardType = (CreditCardType)Int32.Parse(ddlCardType.SelectedValue);

                pi.CreditCardNumber = txtCardNum.Text.Trim();
                pi.ExpirationMonth = Int32.Parse(ddlExpMo.SelectedValue);
                pi.ExpirationYear = Int32.Parse(ddlExpYr.SelectedValue);
                pi.FacilityID = new Guid(ViewState[_facilityIDKey].ToString());
                pi.FamilyID = new Guid(ViewState[_familyIDKey].ToString());
                pi.NameOnCard = txtName.Text.Trim();
                pi.OrderID = Guid.NewGuid();
                pi.PaymentType = cardType.ToMpsPaymentType();
                pi.PurchaseAmount = Math.Abs(Decimal.Parse(txtAmount.Text));
                pi.Cvc = txtCvc.Text.Trim();
                pi.IsCardPresent = cbCardPresent.Checked;


                if (pi.PurchaseAmount >= 0.01m)
                {
                    MerchantProcessingClient svc = new MerchantProcessingClient();

                    try
                    {
                        ProcessingResult result;

                        logger.Debug("Processing transaction (Session Token: {0}) for Facility: {1}, Family: {2}, Purchase Amount{3}",
                                            Session[_postBackTokenKey], pi.FacilityID, pi.FamilyID, pi.PurchaseAmount);

                        if (!isRefund)
                            result = svc.AuthorizePayment(pi);
                        else
                            result = svc.RefundTransaction(pi);

                        if (result.Approved)
                        {
                            //Signal Oasis that it can continue
                            StringBuilder scriptFormat = new StringBuilder();
                            scriptFormat.AppendLine("window.external.OrderID = '{0}';");
                            scriptFormat.AppendLine("window.external.AuthCode = '{1}';");
                            scriptFormat.AppendLine("window.external.AmountCharged = {2};");
                            scriptFormat.AppendLine("window.external.SetPaymentDateFromBinary('{3}');");    //Had to script Int64 as string or it caused an overflow exception for some reason
                            scriptFormat.AppendLine("window.external.CcLast4 = '{4}';");
                            scriptFormat.AppendLine("window.external.SetCreditCardType({5});");
                            scriptFormat.AppendLine("window.external.CardPresent = {6};");
                            scriptFormat.AppendLine("window.external.OnPaymentProcessed();");

                            script = String.Format(scriptFormat.ToString(), result.OrderID, result.AuthCode, result.TransAmount, result.TransDate.ToBinary(),
                                                         (result.MaskedCardNum == null ? String.Empty : result.MaskedCardNum.Replace("*", "")), (int)cardType,
                                                         pi.IsCardPresent.ToString().ToLower());

                            processed = true;   //Don't allow processing again
                        }
                        else
                        {
                            //log and display errors
                        }
                    }
                    catch (Exception ex)
                    {
                        //log, email, and display errors
                    }
                }
                else
                    lblError.Text = "Transaction Amount is zero or too small to process.";
            }
            catch (Exception ex)
            {
                //log, e-mail, and display errors
            }
        }

        this.ClientScript.RegisterStartupScript(this.GetType(), "PaymentApprovedScript", script, true);

        //Session[_isProcessingKey] = processed;  //Set is processing back to false if there was an error
        if (!processed)
            Session[_postBackTokenKey] = null;   //Clear postback token if there was an error to allow re-submission
    }

    private bool IsProcessing()
    {
        bool isProcessing = false;
        Guid postbackToken = new Guid(HFRequestToken.Value);

        // This won't prevent simultaneous POSTs because the second could read the value from 
        // session before the first writes it to session. It will help eliminate duplicate posts
        // if the user is messing with the back button or refreshing.
        if (Session[_postBackTokenKey] != null && (Guid)Session[_postBackTokenKey] == postbackToken)   
            isProcessing = true;
        else
            Session[_postBackTokenKey] = postbackToken;

        return isProcessing;
    }

1 Ответ

1 голос
/ 10 февраля 2012

Я помню, что когда-то подобное происходило однажды (но не с кредитными картами). К сожалению, я не помню, что вызвало это - но я чувствую, как будто это было связано с браузером и не в моем контроле, например. что-то в некоторых браузерах вызывало двойную передачу, и пользователь даже не осознавал этого.

Но решение состоит в том, чтобы справиться с этой ситуацией безопасным образом. Даже если нет причин, по которым (например) автоматизированный процесс должен или должен работать на вашей странице, предположите, что это может быть. Может быть, кто-то использует плагин для заполнения форм, который автоматически отправляет? Или, может быть, у них просто какая-то глючная надстройка или мышь с плохим контактом на левой кнопке. Кажется странным, но кто знает, что может делать конечный пользователь, который может неосознанно обойти любые ваши клиентские средства защиты.

Предположим, что кто-то может нажать URL вашего сообщения дважды подряд (или 100 раз подряд). Потому что, на самом деле, независимо от того, какие средства защиты на стороне клиента у вас есть, они могут. Не беспокойся о клиенте. Вместо этого на сервере, перед началом транзакции, получите поточно-ориентированную блокировку, установите флаг, связанный с их сеансом, который указывает, что транзакция уже выполняется, и завершите работу, если этот флаг найден.

Если по какой-то причине вы не можете доверять сеансу, просто убедитесь, что данные уникальны, прежде чем начинать.

(редактировать для комментария) Если вы переходите в ситуацию, когда у вас есть несколько SQL-серверов, отвечающих за управление сеансом (или, вообще, у вас нет абсолютного способа получить гарантированную блокировку обычными средствами), тогда вам следует перейти для радости, что вы зарабатываете столько денег, и нанимайте эксперта, чтобы решить эту проблему для вас :) В то же время, не беспокойтесь об этом, если это действительно проблема, с которой вы столкнетесь в ближайшее время.

На простом уровне, вот как я бы это сделал (с одним веб-сервером). Похоже, вы уже знаете, как это сделать, но в любом случае ...

public class MakeMoney() {

    private static object locker=new Object();

    public void DoTransaction(SaleData data) {
        lock(locker) {
            if (SessionLocked) {
                throw new Exception("Already in progress");
                /// or just exit however you want
            }
            LockSession();
        }    

        Profit();

        UnlockSession();
    }
}

Реализация LockSession, UnlockSession и SessionLocked просто связана с окружающей средой. С одним сервером Session или HttpContext.Cache, вероятно, в порядке. Даже если задействовано несколько серверов, вы можете создать один нераспределенный сервер, который отвечает только за предоставление блокировок - даже крупномасштабный веб-сайт (если вы не делаете миллионы продаж в минуту!) Должен иметь возможность работать имея только это на одном сервере.

Масштабируемость - это проблема, но если вы реализуете ее каким-либо разумно инкапсулированным способом, это должно быть просто, чтобы заменить контроллер для управления блокировками, если вы когда-нибудь окажетесь в этой великолепной ситуации.

...