有没有PayPal IPN的示例?

12

我有一个 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;
        }

但在第三步中,我试图将我的模型作为json发布,但Paypal返回的是HTML页面而不是VALIDATED或INVALID。我发现我必须使用 application/x-www-form-urlencoded 并且参数必须按相同的顺序排列。

我该如何获取请求URL?

我会使用查询URL,并添加&cmd=_notify-validate


这篇CodeProject上的示例(http://www.codeproject.com/Tips/84538/Setting-up-PayPal-Instant-Payment-Notification-IPN)有帮助吗? - Jason Z
此外,这是 asp.net 的 IPN 示例在 GitHub 上的链接(https://github.com/paypal/ipn-code-samples/blob/master/paypal_ipn.asp)。(我的上一个回复中应该包含这个信息)。 - Jason Z
谢谢。请查看我的编辑。 - Marc
请参见PayPal开发人员网站上的“从PayPal接收到一个INVALID消息”页面。它详细说明了响应URL的格式。正如您所述,它必须以cmd=_notify-validate在其他URL参数之前以完全相同的顺序包括您收到的所有URL参数。 - Jason Z
可能是ASP.NET MVC的Paypal IPN监听器的重复问题。 - Michal Hosala
显示剩余2条评论
5个回答

13
基于被接受的答案,我编写了以下代码来实现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"

问题是我的帐户设置为使用PayPal默认的charset == windows-1252。然而,FormUrlEncodedContent使用UTF-8进行编码,因此由于包含像"ř"这样的国际字符,导致验证失败。解决方案是将charset设置为UTF-8,可以在“个人资料”>“我的售卖工具”>“PayPal按钮语言编码”>“更多选项”中完成,参见此SO主题


1
如果这段代码以前能够工作,现在已经不能了。PayPal要求验证数据必须按照相同的顺序和在cmd变量之前回传。这段代码不符合任何这些要求。 - bilal.haider
1
为什么在再次使用请求数据之前,您要向PayPal发布一个空白请求? - rdans
不是这样的。为什么一个空的帖子会阻止Paypal重新发送IPN呢?请求中没有信息告诉Paypal该帖子是哪笔交易,那么它怎么知道不要再次发送呢? - rdans
1
@rdans 好的,我明白你的观点并且不得不承认我不确定,需要再花些时间研究。请看一下我在第二高票答案下面的评论,似乎我和你想的一样... - Michal Hosala
2
太棒了!我在这种情况下很迷茫,改变编码是解决问题的方法。谢谢。 - HolloW
显示剩余2条评论

6

这是我的代码

如果有问题,请随意检查

        [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;
        }

1
感谢您提供的有用信息,但我想知道第二步是否真的必要。我查看了IPN代码示例,并没有看到两个单独的POST消息,只有第三步中的一个。 - Michal Hosala
尽管PayPal的文档对此也没有明确说明,但在查看“接收第一条通知”(https://developer.paypal.com/docs/classic/ipn/gs_IPN/)时,我看到步骤1是“收到来自PayPal的通知后,发送一个空的HTTP 200响应。”,但是另一份文档(https://developer.paypal.com/docs/classic/ipn/ht_ipn/)根本没有提到这一步骤,这也是我在代码示例中看到的... - Michal Hosala
我知道这个问题已经很老了,但是只是为了澄清一下。我认为第二步被误解了。PayPal是说你对初始IPN的响应应该是一个空的200响应,而不是你需要发送另一个空的200请求。 - kim3er
此外,此代码似乎正在验证冗余的空请求,而不是实际的IPN。 - kim3er
这段代码中有一些明显的编程错误。你不应该创建新的HttpClient,而是在构造函数中注入它并重复使用它。否则,你会打开许多套接字并浪费资源。然后你使用了一个异步方法,但没有使用await,而是调用.Result以同步运行它。我相信在某些情况下,这可能会导致死锁。除此之外,实际逻辑是否正常工作?或者有更好的代码吗? - Etienne Charland
@EtienneCharland 写一个回答 - Luke

3
我也在寻找与OP最初的问题类似的解决方案是否有任何IPN监听器控制器示例?至少有一个PaypalIPNBindingModel来绑定Paypal查询。,然后我找到了这个页面。我尝试了这个主题中提到的其他解决方案,它们都有效,但我真的需要PayPal查询到模型的解决方案,所以我一直在谷歌上搜索,直到我偶然发现了Carlos Rodriguez的创建PayPal IPN Web API端点博客文章。

以下是Carlos所做的概述:

  1. Create a model. Base the properties you'll define in the model from the ipn response you'll get from PayPal.

    public class IPNBindingModel
    {
        public string PaymentStatus { get; set; }
        public string RawRequest { get; set; }
        public string CustomField { get; set; }    
    }
    
  2. Create a PayPal Validator class.

    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. Create your controller.

    [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. Create a model binder that will define how Web Api will automatically create the model for you.

    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. Register your model binder to WebApiConfig.cs. config.BindParameter(typeof(IPNBindingModel), new IPNModelBinder());

希望这篇文章对其他人有所帮助。感谢 Carlos Rodriguez 提供的出色代码。

2

在Michal Hosala的答案的基础上,成功与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 Hosala
1
PayPal集成指南中,我们可以找到以下内容:"在返回的消息前缀中加入cmd=_notify-validate变量,但不要更改消息字段、字段顺序或原始消息的字符编码。" - bilal.haider
是的,我知道Paypal集成指南上是怎么说的,但在现实世界中,依照HTML规范来看,顺序并不重要。 - Michal Hosala
@MichalHosala 我之前做了这个集成,所以不能确定,但我想当我使用字典时没有收到确认。 - bilal.haider

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接