带有禁用按钮的ASP.NET具有重复的回发

时间:2012-02-10 18:51:05

标签: asp.net postback duplication

我有一个asp.net页面,允许输入信用卡信息和付款金额来授权付款。大约两周前突然间,我们开始收到双重收费的报告,但我们没有对页面进行任何更改。该页面已设置为在单击时禁用提交按钮。在尝试解决问题时,我已经在单击按钮时在页面上设置了一个标志,这样如果设置了标志,它将不允许按钮回发(这是我们在另一个页面上使用的方法)这没有问题),但它会继续发生。

有几个原因可以解释为什么我认为用户刷新页面是一个极不可能的问题来源。首先,我们在WPF Web浏览器控件中显示该页面,它与其所在的窗口匹配,并且唯一的迹象表明它甚至是一个网页是回发的点击噪音,如果您要右键单击正文,或者是否有页面错误。唯一的刷新或后退按钮位于浏览器的上下文菜单中。接下来,我可以认为用户没有想要刷新或返回的动机,除非他们收到页面错误,但他们报告在此过程中没有收到任何错误。最后,我采取措施避免在服务器端重复回发,方法是在会话中放置一个令牌并在处理卡之前检查它。因此,用户必须刷新并点击“重试”按钮的速度比第一个请求可以将令牌写入会话状态的速度快。实现这一目标的最快方法是按提交,F5,连续输入。我不想忽视我知道它可能发生的唯一方式,但似乎可以肯定地说这不是正在发生的事情。最后,在回发页面时,通过脚本对象向WPF应用程序发出信号,表明它可以关闭,因此在浏览器消失之前,用户无法在回发后在页面上执行任何操作。

唯一的问题是,我不知道发生了什么。不知何故,一个提交刚刚通过javascript安全卫士和服务器端令牌安全卫士并得到双重收费,我不知道如何。他们被记录为彼此在2秒内发生。我已经验证我们的WPF应用程序代码没有调用Refresh或以其他方式控制浏览器的导航。有人有什么想法吗?

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 个答案:

答案 0 :(得分:1)

我记得曾经发生过这样的事情(虽然不是用信用卡)。不幸的是我不记得是什么导致了它 - 但我觉得它好像与浏览器有关而不在我的掌控之中,例如某些浏览器中的某些东西导致双重提交而用户甚至没有意识到它。

但解决方案是以竞赛条件安全的方式处理这种情况。即使没有理由(例如)自动化流程应该或应该对您的页面进行操作,也可以假设它。也许有人正在使用自动提交的插件表单填充程序?或者他们可能只是有一个某种类型的错误加载项,或者左键上有一个接触不良的鼠标。看起来很奇怪,但是谁知道最终用户可能会做什么,可能会在不知不觉中规避任何客户端保护措施。

假设某人可以连续两次(或连续100次)点击您的帖子网址。事实上,无论你有什么客户端保护,他们都可以。不要担心客户。相反,在服务器上,在启动事务之前,获取线程安全锁,设置与其会话关联的标志,指示事务正在进行中,如果找到该标志则退出。

如果由于某种原因无法信任会话,则只需在启动前验证数据是否唯一。

(编辑每条评论)如果你改变到你有多个SQL服务器负责会话管理的情况(或者,通常,你没有绝对的方法来获得传统方法的保证锁)那么你应该跳很高兴你赚了这么多钱,聘请专家为你解决这个问题:)在此期间,不要担心,除非你真的遇到问题,否则很快就会面临。

简单来说,这就是我将如何做到这一点(使用单个Web服务器)。听起来你可能已经知道如何做到这一点但无论如何......

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();
    }
}

LockSessionUnlockSessionSessionLocked的实施只与环境有关。使用一台服务器,SessionHttpContext.Cache可能没问题。即使涉及多个服务器,您也可以创建一个仅负责提供锁定的非分布式服务器 - 即使是大容量网站(除非您每分钟进行数百万次销售!)应该能够处理只有在一台服务器上。

可扩展性是一个问题 - 但如果你以任何合理的封装方式实现它,如果你发现自己处于那种光荣的境地,那么交换控制器来管理锁定应该是一块蛋糕。