【问题标题】:Is there any sample for PayPal IPN是否有 PayPal IPN 的样本
【发布时间】:2014-12-25 15:49:39
【问题描述】:

我有一个 Asp.Net WEB API 2 项目,我想实现一个即时支付通知 (IPN) 侦听器控制器。

我找不到任何示例和 nuget 包。我只需要确认用户使用 Paypal 上的标准 html 按钮付款。这很简单。

所有 nuget 包都用于创建发票或自定义按钮。这不是我需要的

paypal 上的示例适用于经典的 asp.net,而不适用于 MVC 或 WEB API MVC

我确信已经有人这样做了,当我开始编码时,我有一种重新发明轮子的感觉。

有没有 IPN 监听控制器示例?

至少有一个 PaypalIPNBindingModel 来绑定 Paypal 查询。

    [Route("IPN")]
    [HttpPost]
    public IHttpActionResult IPN(PaypalIPNBindingModel model)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest();
        }

        return Ok();
    }

编辑

到目前为止,我有以下代码

        [Route("IPN")]
        [HttpPost]
        public void IPN(PaypalIPNBindingModel model)
        {
            if (!ModelState.IsValid)
            {
                // if you want to use the PayPal sandbox change this from false to true
                string response = GetPayPalResponse(model, true);

                if (response == "VERIFIED")
                {

                }
            }
        }

        string GetPayPalResponse(PaypalIPNBindingModel model, bool useSandbox)
        {
            string responseState = "INVALID";

            // Parse the variables
            // Choose whether to use sandbox or live environment
            string paypalUrl = useSandbox ? "https://www.sandbox.paypal.com/"
            : "https://www.paypal.com/";

            using (var client = new HttpClient())
            {
                client.BaseAddress = new Uri(paypalUrl);
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));

                //STEP 2 in the paypal protocol
                //Send HTTP CODE 200
                HttpResponseMessage response = client.PostAsJsonAsync("cgi-bin/webscr", "").Result;

                if (response.IsSuccessStatusCode)
                {
                    //STEP 3
                    //Send the paypal request back with _notify-validate
                    model.cmd = "_notify-validate";
                    response = client.PostAsync("cgi-bin/webscr", THE RAW PAYPAL REQUEST in THE SAME ORDER ).Result;

                    if(response.IsSuccessStatusCode)
                    {
                        responseState = response.Content.ReadAsStringAsync().Result;
                    }
                }
            }

            return responseState;
        }

但是对于第 3 步,我尝试将我的模型发布为 json,但 paypal 返回一个 HTML 页面而不是 VALIDATED 或 INVALID。我发现我必须使用application/x-www-form-urlencoded,并且它的参数顺序相同。

如何获取请求 URL?

我会使用查询网址并将&cmd=_notify-validate 添加到其中

【问题讨论】:

  • CodeProject 上的this sample 有帮助吗?
  • 另外,here's the IPN sample on GitHub 用于 asp.net。 (意味着在我之前的回复中包含这一点)。
  • 谢谢。请查看我的编辑
  • 查看 PayPal 开发者网站上的 Receiving an INVALID message from PayPal 页面。它准确地解释了您的响应 URL 应如何格式化。正如您所说,它必须包含您在通知中以完全相同的顺序收到的所有 URL 参数,但 cmd=_notify-validate 其他 URL 参数之前。
  • 我的问题是我有一个 PaypalIPNBindingModel 对象而不是原始请求。我不能确定我使用的是相同的顺序。我正在尝试弄清楚如何获取原始帖子数据。

标签: c# asp.net-web-api paypal paypal-ipn


【解决方案1】:

这里有一个官方的 c# 示例: https://github.com/paypal/ipn-code-samples 在路径\c#\paypal_ipn_mvc.cs

C# 示例显示了一个 ASP.NET MVC 控制器,其中包含一个响应 IPN 的操作。

【讨论】:

    【解决方案2】:

    我也在寻找类似于 OP 的原始问题 Is there any IPN listener controller example? At least a PaypalIPNBindingModel to bind the Paypal query. 的解决方案,然后我来到了这个页面。我尝试了这个线程中提到的其他解决方案,它们都有效,但我真的需要 PayPal 查询到模型的解决方案,所以我在谷歌上搜索,直到我偶然发现了 Carlos Rodriguez 的 Creating a PayPal IPN Web API Endpoint 博文。

    以下是 Carlos 所做工作的概述:

    1. 创建一个模型。基于您将从 PayPal 获得的 ipn 响应在模型中定义的属性。

      public class IPNBindingModel
      {
          public string PaymentStatus { get; set; }
          public string RawRequest { get; set; }
          public string CustomField { get; set; }    
      }
      
    2. 创建一个 PayPal 验证器类。

      public class PayPalValidator
      {
          public bool ValidateIPN(string body)
          {
              var paypalResponse = GetPayPalResponse(true, body);
              return paypalResponse.Equals("VERIFIED");
          }
      
          private string GetPayPalResponse(bool useSandbox, string rawRequest)
          {
              string responseState = "INVALID";
              string paypalUrl = useSandbox ? "https://www.sandbox.paypal.com/"
              : "https://www.paypal.com/";
      
              ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
              using (var client = new HttpClient())
              {
                  client.BaseAddress = new Uri(paypalUrl);
                  client.DefaultRequestHeaders.Accept.Clear();
                  client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));
                  HttpResponseMessage response = client.PostAsJsonAsync("cgi-bin/webscr", "").Result;
                  if (response.IsSuccessStatusCode)
                  {
                      rawRequest += "&cmd=_notify-validate";
                      HttpContent content = new StringContent(rawRequest);
                      response = client.PostAsync("cgi-bin/webscr", content).Result;
                      if (response.IsSuccessStatusCode)
                      {
                          responseState = response.Content.ReadAsStringAsync().Result;
                      }
                  }
              }
              return responseState;
          }
      }
      
    3. 创建你的控制器。

      [RoutePrefix("paypal")]
      public class PayPalController : ApiController
      {
          private PayPalValidator _validator;
      
          public PayPalController()
          {
             this._validator = new PayPalValidator();
          }
      
          [HttpPost]
          [Route("ipn")]
          public void ReceiveIPN(IPNBindingModel model)
          {
              if (!_validator.ValidateIPN(model.RawRequest)) 
                  throw new Exception("Error validating payment");
      
              switch (model.PaymentStatus)
              {
      
                  case "Completed":
                      //Business Logic
                      break;
              }
         }
      }
      
    4. 创建一个模型绑定器,它将定义 Web Api 如何为您自动创建模型。

      public class IPNModelBinder : IModelBinder
      {
          public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
          {
              if (bindingContext.ModelType != typeof(IPNBindingModel))
              {
                 return false;
              }
          var postedRaw = actionContext.Request.Content.ReadAsStringAsync().Result;
      
          Dictionary postedData = ParsePaypalIPN(postedRaw);
          IPNBindingModel ipn = new IPNBindingModel
          {
              PaymentStatus = postedData["payment_status"],
              RawRequest = postedRaw,
              CustomField = postedData["custom"]
          };
      
          bindingContext.Model = ipn;
          return true;
      }
      
      private Dictionary ParsePaypalIPN(string postedRaw)
      {
          var result = new Dictionary();
          var keyValuePairs = postedRaw.Split('&');
          foreach (var kvp in keyValuePairs)
          {
              var keyvalue = kvp.Split('=');
              var key = keyvalue[0];
              var value = keyvalue[1];
              result.Add(key, value);
          }
      
          return result;
      }
      }
       }
      
    5. 将您的模型绑定器注册到 WebApiConfig.cs。 config.BindParameter(typeof(IPNBindingModel), new IPNModelBinder());

    希望这对其他人有所帮助。感谢 Carlos Rodriguez 的精彩代码。

    【讨论】:

    • 哦,这就是我的博客突然获得浏览量的原因吗?谢谢你:-)
    【解决方案3】:

    扩展 Michal Hosala 的 answer,与 PayPal 成功握手需要两件事

    首先,在向 PayPal 发出请求之前设置安全协议

    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
    

    其次,避免使用字典,因为为了验证,PayPal 要求数据以相同的顺序回发,并以 cmd 变量开头。我最终这样做了

    Request.InputStream.Seek(0, SeekOrigin.Begin);
    string rawRequestBody = new StreamReader(Request.InputStream).ReadToEnd();
    var ipnVarsWithCmd = rawRequestBody.Split('&').Select(x => new KeyValuePair<string, string>(x.Split('=')[0], x.Split('=')[1])).ToList();
    ipnVarsWithCmd.Insert(0, new KeyValuePair<string, string>("cmd", "_notify-validate"));
    

    【讨论】:

    • “PayPal 要求数据以相同的顺序回发,并以 cmd 变量开头” - 有任何证据吗?我相信 HTML 规范说输入的顺序无关紧要,所以我认为字典在这里就可以了。你真的试过了吗?以及如何设置安全协议?你能在那里扩展你的答案吗?显然我没有设置它并重申,我的实现工作得很好......
    • @Michal 在PayPal Integration Guide 我们可以找到这个。 "使用 cmd=_notify-validate 变量为返回的消息添加前缀,但不要更改消息字段、字段顺序或原始消息的字符编码。"
    • 是的,我知道 Paypal 集成指南所说的内容,但我是说在现实世界中,根据 HTML 规范,顺序应该无关紧要。
    • @MichalHosala 我之前做过这个整合,所以不能肯定,但我想我在使用字典时没有得到确认。
    【解决方案4】:

    这是我的代码

    如有问题请随时查看

            [Route("IPN")]
            [HttpPost]
            public IHttpActionResult IPN()
            {
                // if you want to use the PayPal sandbox change this from false to true
                string response = GetPayPalResponse(true);
    
                if (response == "VERIFIED")
                {
                    //Database stuff
                }
                else
                {
                    return BadRequest();
                }
    
                return Ok();
            }
    
            string GetPayPalResponse(bool useSandbox)
            {
                string responseState = "INVALID";
                // Parse the variables
                // Choose whether to use sandbox or live environment
                string paypalUrl = useSandbox ? "https://www.sandbox.paypal.com/"
                : "https://www.paypal.com/";
    
                using (var client = new HttpClient())
                {
                    client.BaseAddress = new Uri(paypalUrl);
                    client.DefaultRequestHeaders.Accept.Clear();
                    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));
    
                    //STEP 2 in the paypal protocol
                    //Send HTTP CODE 200
                    HttpResponseMessage response = client.PostAsJsonAsync("cgi-bin/webscr", "").Result;
    
                    if (response.IsSuccessStatusCode)
                    {
                        //STEP 3
                        //Send the paypal request back with _notify-validate
                        string rawRequest = response.Content.ReadAsStringAsync().Result;
                        rawRequest += "&cmd=_notify-validate";
    
                        HttpContent content = new StringContent(rawRequest);
    
                        response = client.PostAsync("cgi-bin/webscr", content).Result;
    
                        if(response.IsSuccessStatusCode)
                        {
                            responseState = response.Content.ReadAsStringAsync().Result;
                        }
                    }
                }
    
                return responseState;
            }
    

    【讨论】:

    • 感谢您的帮助,但我想知道第 2 步是否真的有必要。我正在查看IPN code samples,但我没有看到两条单独的 POST 消息,只是来自 STEP 3 的一条。
    • 尽管 PayPal 文档对此也不是很清楚,但查看 "Receiving your first notification" 我看到第 1 步“收到来自 PayPal 的通知后,发送一个空的 HTTP 200 响应。”,但是other piece of documentation 根本没有提到这一步,这也是我在代码示例中看到的......
    • 我知道这个问题已经很老了,但只是为了澄清。我相信第二步在这里被误解了。第二步,是 PayPal 说您对初始 IPN 的响应应该是空的 200 响应,而不是您需要发送额外的空 200 请求。
    • 此外,此代码似乎正在验证冗余的空请求,而不是实际的 IPN。
    • 这段代码中有一些明显的编程错误。您永远不应该创建新的 HttpClient,而是将其注入构造函数并重新使用它。否则,您会打开许多​​套接字并浪费资源。然后你使用一个没有等待的异步方法,而是调用 .Result 来同步运行它。我相信这在某些情况下会导致死锁。除此之外,实际的逻辑是否在发挥作用,还是在某个地方有更好的代码?
    【解决方案5】:

    根据接受的答案,我想出了以下代码,为 ASP.NET MVC 实现 IPN 侦听器。该解决方案已部署并且似乎可以正常工作。

    [HttpPost]
    public async Task<ActionResult> Ipn()
    {
        var ipn = Request.Form.AllKeys.ToDictionary(k => k, k => Request[k]);
        ipn.Add("cmd", "_notify-validate");
    
        var isIpnValid = await ValidateIpnAsync(ipn);
        if (isIpnValid)
        {
            // process the IPN
        }
    
        return new EmptyResult();
    }
    
    private static async Task<bool> ValidateIpnAsync(IEnumerable<KeyValuePair<string, string>> ipn)
    {
        using (var client = new HttpClient())
        {
            const string PayPalUrl = "https://www.paypal.com/cgi-bin/webscr";
    
            // This is necessary in order for PayPal to not resend the IPN.
            await client.PostAsync(PayPalUrl, new StringContent(string.Empty));
    
            var response = await client.PostAsync(PayPalUrl, new FormUrlEncodedContent(ipn));
    
            var responseString = await response.Content.ReadAsStringAsync();
            return (responseString == "VERIFIED");
        }
    }
    

    编辑:

    让我分享一下我的经验 - 到目前为止,上面的代码运行良好,但突然对于它正在处理的一个 IPN 失败,即responseString == "INVALID"

    问题原来是我的帐户设置为使用charset == windows-1252,这是 PayPal 的默认设置。但是,FormUrlEncodedContent 使用 UTF-8 进行编码,因此由于“ř”等国家字符,验证失败。解决方案是将charset设置为UTF-8,可以在个人资料>我的销售工具>贝宝按钮语言编码>更多选项中完成,见this SO thread

    【讨论】:

    • 对 UTF-8 的伟大呼吁。有点难找,但它就在那里。
    • 如果这段代码过去可以工作,现在就不行了。 PayPal 要求验证数据以以相同顺序回发并在 cmd 变量之前。此代码不满足任何这些要求。
    • 为什么在再次发布请求数据之前先向贝宝发布一个空白请求?
    • @rdans 好吧,我明白你的意思,不得不承认我不确定,将不得不花更多时间在这上面。请在投票第二高的答案下方查看我的 cmets,好像我想知道的和你一样......
    • 太棒了。我迷失了这些情况,改变编码是我的情况的解决方案。谢谢。
    猜你喜欢
    • 2015-12-01
    • 2013-07-10
    • 2015-05-06
    • 2014-03-14
    • 2011-02-14
    • 2012-12-28
    • 2016-07-10
    • 2013-02-25
    • 1970-01-01
    相关资源
    最近更新 更多