我有一个 asp.net 页面,允许输入信用卡信息和付款金额以授权付款。大约 2 周前突然间,我们开始收到双重收费的报告,但我们没有对页面进行任何更改。该页面已设置为在单击时禁用提交按钮。在尝试解决问题时,我还在单击按钮时在页面上设置了一个标志,这样如果设置了标志,它将不允许按钮回发(这是我们在另一个页面上使用的方法这没有问题),但它继续发生。
我认为用户刷新页面是极不可能的问题根源有几个原因。首先,我们在 WPF Web 浏览器控件中显示页面,它与它所在的窗口匹配,并且它甚至是网页的唯一指示是回发的点击噪音,如果您要右键单击正文,或者如果有页面错误。唯一的刷新或返回按钮位于浏览器的上下文菜单中。接下来,我认为用户没有动机想要刷新或返回,除非他们收到页面错误,但他们报告在此过程中没有收到任何错误。最后,我采取措施避免服务器端重复回发,方法是在会话中放置一个令牌并在处理卡之前检查它。所以用户必须刷新并点击“重试” 比第一个请求更快的按钮可以将令牌写入会话状态。实现这一目标的最快方法是按提交,F5,连续输入。我讨厌忽略我知道它可能发生的唯一方式,但可以肯定地说这不是正在发生的事情。最后,在回发页面时,通过脚本对象向 WPF 应用程序发出信号,表明它可以关闭,因此用户在回发之后在浏览器消失之前无法在页面上执行任何操作。
唯一的问题是,我不知道发生了什么。不知何故,提交刚刚通过了 javascript 安全防护和服务器端令牌安全防护并被双重收费,我不知道如何。它们被记录为在 2 秒内发生。我已经验证我们的 WPF 应用程序的代码没有调用 Refresh 或以其他方式控制浏览器的导航。有人有想法么?
更新这里是一些相关的代码:
<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;
}