2012-02-10 2 views
1

나는 asp.net 페이지에서 신용 카드 정보와 결제 금액을 입력하여 결제 권한을 부여 할 수 있습니다. 약 2 주 전 갑자기 우리는 이중 청구에 대한보고를 받기 시작했지만 페이지를 변경하지 않았습니다. 클릭 할 때 제출 버튼을 사용 중지하도록 페이지가 이미 설정되었습니다. 이 문제를 해결하기 위해 버튼을 클릭 할 때 플래그를 설정 했으므로 플래그가 설정되어 있으면 버튼이 다시 게시 할 수 없습니다 (다른 페이지에서 사용하는 방법입니다). 그다지 문제는 없습니다). 그러나 그것은 계속 발생합니다.중복 된 포스트 백이있는 비활성화 된 버튼이있는 ASP.NET

사용자가 페이지를 새로 고침하여 문제를 일으킬 가능성이 거의없는 것으로 보이는 몇 가지 이유가 있습니다. 먼저 WPF 웹 브라우저 컨트롤에 페이지를 표시하고 창과 일치하며 웹 페이지 일 수도있는 유일한 표시는 포스트 백의 클릭 노이즈, 몸을 마우스 오른쪽 버튼으로 클릭 한 경우 또는있을 경우 페이지 오류. 유일한 새로 고침 또는 뒤로 버튼은 브라우저의 컨텍스트 메뉴에 있습니다. 다음으로 페이지 오류가 발생하지 않는 한 사용자가 새로 고침하거나 되돌아 가려는 동기가 없다고 생각할 수 있지만 오류가없는 것으로보고합니다. 마지막으로, 세션에서 토큰을 배치하고 카드를 처리하기 전에이를 점검하여 서버 측에서 중복 된 포스트 백을 피하기위한 조치를 취했습니다. 따라서 사용자는 첫 번째 요청이 세션 상태에 토큰을 쓸 수있는 것보다 "재시도"버튼을 빠르게 새로 고쳐야합니다. 이를 달성하는 가장 빠른 방법은 보도 자료 제출, F5, 모두 연속으로 입력하는 것입니다. 나는 그것이 일어날 수 있다는 것을 알 수있는 유일한 방법을 무시하는 것을 싫어하지만, 이것이 일어나고있는 것이 아니라고 말하는 것이 안전하다고 보입니다. 마지막으로, 페이지를 게시 할 때 스크립팅 객체를 통해 WPF 응용 프로그램에 신호를 보내면 브라우저가 사라지기 전에 다시 게시 한 후 사용자가 페이지에서 아무 것도 할 수 없도록 닫을 수 있습니다.

유일한 문제는 무엇이 일어나고 있는지 모르겠습니다. 어떻게 든 제출은 자바 스크립트 안전 가드와 서버 측 토큰 안전 가드를 지나쳐 버렸고 이중 과금을 받았고 나는 어떻게 될지 전혀 모른다. 그들은 서로 2 초 이내에 일어난 것으로 기록되었습니다. WPF 앱의 코드가 새로 고침을 호출하거나 브라우저 탐색을 제어하지 않는다는 것을 확인했습니다. 누구든지 아이디어가 있습니까?

<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; 
    } 
+0

사용자가 브라우저에서 뒤로를 클릭하고 포스트 백을 다시 제출합니까? –

+0

로마인에게 감사드립니다. 내가 자바 스크립트가 사용자가 완벽하게 시간을 가질 수 있도록 게시 할 때 창을 닫을 수 있다고 WPF 앱에 알리는 것을 잊어 버렸습니다. 백 스페이스 키를 누르면 새로 고침 옵션이 조금 더 나을 가능성이 있지만 발생 및 빈도 (거의 하루에 한 번, 때로는 한 번)의 갑작스런 웨이브와 결합하면 이럴 가능성이 희박해질 수 있습니다. – xr280xr

+1

관련 코드를 볼 수 있습니까? ASPX 마크 업과 모든 자바 스크립트, VB/C# .. –

답변

1

내가 (안 신용 카드로 생각)이 일이 한 번 뭔가를 가진 기억 :

UPDATE 다음은 관련 코드의 일부이다. 불행하게도 그 원인을 기억하지 못합니다.하지만 브라우저와 관련이 있으며 내 제어가 아닌 것처럼 느낍니다. 어떤 브라우저에서는 사용자가 그것을 깨닫지 못하고 이중 제출을하고있었습니다.

그러나 해결책은이 상황을 경쟁 조건에서 안전하게 처리하는 것입니다. 심지어 자동화 된 프로세스가 귀하의 페이지에 대해 작동해야하거나 작동해야하는 이유가 없다고 가정 할 수도 있습니다. 어쩌면 누군가가 자동 제출하는 플러그인 양식 필러를 사용하고 있을까요? 또는 어쩌면 그들은 어떤 종류의 버그 추가 기능을 가지고 있거나 마우스 왼쪽 버튼에 접촉이 나쁜 마우스 일 수 있습니다. 이상하게 보일 지 모르지만, 최종 사용자가 수행 할 수있는 작업을 모르는 채 사용자가 알고있는 클라이언트 측 보호를 우회 할 수 있습니다.

다른 사용자가 귀하의 게시물 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이 좋습니다. 관련된 서버가 여러 개인 경우에도 분당 수백만 개의 판매를하지 않는 한 대용량 웹 사이트 인 경우에도 잠금을 제공 할 책임이있는 단일 비 분산 서버를 만들 수 있습니다. 하나의 서버에 설치하면됩니다.

확장성에 문제가 있습니다. 그러나 합리적으로 캡슐화 된 방식으로 구현하면 잠금 상태를 제어하기 위해 컨트롤러를 교체해야합니다. 그렇다면 영광스러운 상황에서 스스로를 찾아야합니다.

+0

이것은 기본적으로 의미가 있습니다. 실제로, 나는 그것을하는 방법을 모르겠다. 세션에 세마포어를 배치하기위한 코드를 작성했으며 (실제로 최근에 다른 스레드가 있음) 중복 된 포스트 백을 방지하기 위해이 코드를 사용했습니다. 그런 다음 확장성에 대해 생각하기 시작했습니다. SQL 세션으로 변경하거나로드 균형 조정을 위해 여러 서버를 사용하면 더 이상 작동하지 않습니다. 구현 관련 아이디어를 더 많이들을 수 있도록 도와 드리겠습니다. WPF 래핑 된 IE 활성 X 브라우저에서 플러그인/추가 기능을 사용할 수 있습니까? – xr280xr

+0

WPF 래핑 된 주석에 대해 확실하지 않습니다. 즉, 당신이 무슨 말을하고 있는지 전혀 모르겠지만 구현에 대한 자세한 내용은 편집을 참조하십시오. –

+0

당신은 [WPF WebBrowser Control] (http://msdn.microsoft.com/en-us/library/system.windows.controls.webbrowser.)이 있으면 나를 호기심에 빠뜨린 버그가있는 [브라우저] 플러그인/추가 기능에 대해 언급했습니다. aspx)는 IE ActiveX 컨트롤을 랩핑하며 추가 기능과 플러그인이 활성화되어 있습니다. 예를 들어 주셔서 감사합니다. 나는 잠금/모니터 asp.net의 상태가없는 자연 때문에 작동할지 모르겠다.첫 번째 포스트 백이 MakeMoney의 인스턴스를 만들고 두 번째 포스트 백이 MakeMoney의 별도 인스턴스를 생성한다는 것을 의미합니다. 잠긴 코드가 잠긴 인스턴스 내에서만 잠겨 있다고 생각했습니다. 그렇지 않습니까? – xr280xr