У меня есть страница 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;
}