asp.net core 3.1 在条带 HttpPost("webhook") 中获取当前身份用户返回 NULL

问题描述

我已根据 Stripe 的 example

在我的网站中集成了 Stripe checkout 付款

一切正常。我可以验证 webhooks 是否使用 Stripe CLI 并使用 ngrok,对我的 localhost 进行隧道传输。

现在我已经开始实现与身份数据库的交互。 我想在 webhook 触发 session.CustomerId 后将条纹 checkout.session.completed 存储在那里。

为此,我需要访问我的身份数据库

我的代码是:

[HttpPost("webhook")]
    public async Task<IActionResult> Webhook()
    {
        // GET logged current user
        var currentUser = await _userManager.GetUserAsync(User);

        var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();
        Event stripeEvent;
        try
        {
            stripeEvent = EventUtility.ConstructEvent(
                json,Request.Headers["Stripe-Signature"],this.options.Value.WebhookSecret
            );
            Console.WriteLine($"Webhook notification with type: {stripeEvent.Type} found for {stripeEvent.Id}");
        }
        catch (Exception e)
        {
            Console.WriteLine($"Something Failed {e}");
            return BadRequest();
        }



        // Handle the event
        if (stripeEvent.Type == Events.PaymentIntentSucceeded) //Success
        {
            var paymentIntent = stripeEvent.Data.Object as PaymentIntent;
            var subscriptionItem = stripeEvent.Data.Object as Stripe.Subscription;
            var subscriptionInvoice = stripeEvent.Data.Object as Invoice;
            var paidTimestamp = paymentIntent.Created;
            var paidValidityTimestamp = subscriptionItem.CurrentPeriodEnd;
            Console.WriteLine("*********************");
            Console.WriteLine(".....................");
            Console.WriteLine("Payment Intent Succeeded");
            // Then define and call a method to handle the successful payment intent.
            // handlePaymentIntentSucceeded(paymentIntent);

            paymentIntent.ReceiptEmail = currentUser.Email;
            paymentIntent.Shipping.Address.City = currentUser.City;
            paymentIntent.Shipping.Address.PostalCode = currentUser.PostalCode.ToString();
            paymentIntent.Shipping.Address.Line1 = currentUser.Street + " " + currentUser.CivicNumber;

            currentUser.hasPaidQuote = true;
            currentUser.paidOnDate = paidTimestamp;
            currentUser.paidValidity = paidValidityTimestamp;
            currentUser.SubscriptionStatus = paymentIntent.Status;
            currentUser.Status = LoginUserStatus.Approved;
            await _userManager.UpdateAsync(currentUser);

            Console.WriteLine("has paid? " + currentUser.hasPaidQuote);
            Console.WriteLine("paid on date: " + currentUser.paidOnDate.Date.ToString());
            Console.WriteLine("payment validity: " + currentUser.paidValidity.Date.ToString());
            Console.WriteLine("subscription status: " + currentUser.SubscriptionStatus);
            Console.WriteLine("user status: " + currentUser.Status.ToString());
            Console.WriteLine("customer ID: " + paymentIntent.Customer.Id);
            Console.WriteLine("payment intent status: " + paymentIntent.Status);
            Console.WriteLine("invoice status from paymentIntent: " + paymentIntent.Invoice.Status);
            Console.WriteLine("invoice status from subscriptionItem: " + subscriptionItem.LatestInvoice.Status);
            Console.WriteLine("subscription status: " + subscriptionItem.Status);
            Console.WriteLine("invoice status: " + subscriptionInvoice.Paid.ToString());

            Console.WriteLine(".....................");
            Console.WriteLine("*********************");

        }
        else if (stripeEvent.Type == Events.PaymentIntentPaymentFailed) //Fails due to card error
        {
            var paymentIntent = stripeEvent.Data.Object as PaymentIntent;
            Console.WriteLine("*********************");
            Console.WriteLine(".....................");
            Console.WriteLine("Payment Intent requires payment method. Payment Failed due to card error");
            // Then define and call a method to handle the successful attachment of a PaymentMethod.
            // handlePaymentMethodAttached(paymentMethod);

            Console.WriteLine("payment intent status: " + paymentIntent.Status);
            Console.WriteLine("invoice status: " + paymentIntent.Invoice.Status);
            Console.WriteLine(".....................");
            Console.WriteLine("*********************");
        }
        else if (stripeEvent.Type == Events.PaymentIntentRequiresAction) //Fails due to authentication
        {
            var paymentIntent = stripeEvent.Data.Object as PaymentIntent;
            Console.WriteLine("*********************");
            Console.WriteLine(".....................");
            Console.WriteLine("Payment Intent requires actions");
            // Then define and call a method to handle the successful attachment of a PaymentMethod.
            // handlePaymentMethodAttached(paymentMethod);

            Console.WriteLine("payment intent status: " + paymentIntent.Status);
            Console.WriteLine("invoice status: " + paymentIntent.Invoice.Status);
            Console.WriteLine(".....................");
            Console.WriteLine("*********************");
        }
        else if (stripeEvent.Type == Events.PaymentMethodAttached)
        {
            var paymentMethod = stripeEvent.Data.Object as PaymentMethod;
            Console.WriteLine("Payment Method Attached");
            // Then define and call a method to handle the successful attachment of a PaymentMethod.
            // handlePaymentMethodAttached(paymentMethod);
        }
        // ... handle other event types




        if (stripeEvent.Type == "checkout.session.completed")
        {
            var session = stripeEvent.Data.Object as Stripe.Checkout.Session;
            ViewBag.eventID = stripeEvent.Id;

            Console.WriteLine("*********************");
            Console.WriteLine(".....................");
            Console.WriteLine("checkout session completed");
            Console.WriteLine($"Session ID: {session.Id}");

            // Take some action based on session.
            currentUser.StripeCustomerId = session.CustomerId;
            await _userManager.UpdateAsync(currentUser);

            Console.WriteLine("Ssession.CustomerId has been retrieved from session and will be stored into database: " + session.CustomerId);
            Console.WriteLine("StripeCustomerId has been stored into database: " + currentUser.StripeCustomerId);
            Console.WriteLine(".....................");
            Console.WriteLine("*********************");
        }

        return Ok();
    }

我的问题是 var currentUser = await _userManager.GetUserAsync(User); 返回 NULL。

如果我在调试模式下运行代码,我的刹车点上会出现一个带有感叹号的蓝色圆圈。将鼠标悬停在那里我收到通知:“进程或线程自上一步以来发生了变化”。 使用条带的结帐 API 会将我重定向到某个地方,从而丢失了我的会话信息,因此也丢失了指向我的身份数据库的任何链接

如何获取我的变量 currentUser 包含登录用户

我的PaymentsController如下:

public class PaymentsController : Controller
{
    public readonly IOptions<StripeOptions> options;
    private readonly IStripeClient client;
    private UserManager<VivaceApplicationUser> _userManager;

    private readonly IConfiguration Configuration;


    public IActionResult Index()
    {
        return View();
    }

    public IActionResult canceled()
    {
        return View();
    }

    public IActionResult success()
    {
        return View();
    }

    public PaymentsController(IOptions<StripeOptions> options,IConfiguration configuration,UserManager<VivaceApplicationUser> userManager)
    {
        this.options = options;
        this.client = new StripeClient(this.options.Value.SecretKey);
        _userManager = userManager;
        Configuration = configuration;
    } .......

}

我的 [HttpGet("checkout-session")]

[HttpGet("checkout-session")]
    public async Task<IActionResult> CheckoutSession(string sessionId)
    {
        var currentUser = await _userManager.GetUserAsync(HttpContext.User);
        var service = new SessionService(this.client);
        var session = await service.GetAsync(sessionId);

        // Retrieve prices of product
        var product = new PriceService();
        var productBasic = product.Get(Configuration.GetSection("Stripe")["BASIC_PRICE_ID"]);
        var productMedium = product.Get(Configuration.GetSection("Stripe")["MEDIUM_PRICE_ID"]);
        var productPremium = product.Get(Configuration.GetSection("Stripe")["PREMIUM_PRICE_ID"]);

        int quoteBasic = (int)productBasic.UnitAmount;
        int quoteMedium = (int)productMedium.UnitAmount;
        int quotePremium = (int)productPremium.UnitAmount;

        int selectedplan = (int)session.AmountTotal;
       
        if (!string.IsNullOrEmpty(session.Id))
        {
            // storing stripe customerId into the User database
            // this will be done if checkout.session.completed
            //currentUser.StripeCustomerId = session.CustomerId;
            
            if (selectedplan == quoteBasic)
            {
                currentUser.Subscription = Models.VivaceUsers.Subscription.Basic;
            }
            else if (selectedplan == quoteMedium)
            {
                currentUser.Subscription = Models.VivaceUsers.Subscription.Medium;
            }
            else if (selectedplan == quotePremium)
            {
                currentUser.Subscription = Models.VivaceUsers.Subscription.Premium;
            }

            await _userManager.UpdateAsync(currentUser);
        }

        return Ok(session);
    }

在“结帐会话”中,我可以简单地使用以下方法检索 currentUser:

var currentUser = await _userManager.GetUserAsync(HttpContext.User);

如何在 webhook 方法中启动并运行它?

解决方法

问题是当您收到来自 Stripe 的 POST 请求时,没有用户会话。身份验证会话可能由 cookie 管理并由您客户的浏览器保存。直接从 Stripe 接收 POST 请求时,该请求将不包含与您的已验证用户相关的任何 cookie。

不是使用以下方法查找当前用户,而是需要另一种方法将 Checkout 会话与数据库中的用户相关联。

var currentUser = await _userManager.GetUserAsync(HttpContext.User);

有两种标准方法可以解决这个问题。

  1. 将用户 ID 存储在结帐会话的元数据中。

  2. 将结帐会话的 ID 存储在您的数据库中,并与当前用户有某种关系。

许多用户在选项 1 上都取得了成功,创建结帐会话时它可能类似于以下内容:

var currentUser = await _userManager.GetUserAsync(HttpContext.User);
var options = new SessionCreateOptions
{
    SuccessUrl = "https://example.com/success",Metadata = new Dictionary<string,string>
    {
        {"UserId",currentUser.ID},}
    //...
};

然后在从 webhook 处理程序中查找用户时:

var currentUser = await _userManager.FindByIdAsync(checkoutSession.Metadata.UserId);
,

感谢 CJAV,我能够找到一种方法来启动和运行它。我还在 Unable to retrieve a session's variable values in a Stripe webhookReconciling "New Checkout" Session ID with Webhook Event data 处找到了非常有价值的信息。

在我的 [HttpPost("create-checkout-session")] 中,我添加了对 ClientReferenceId = currentUser.Id 的引用,如下所示:

var options = new SessionCreateOptions
{
    .....

    PaymentMethodTypes = new List<string>
    {
        "card",},Mode = "subscription",LineItems = new List<SessionLineItemOptions>
    {
        new SessionLineItemOptions
        {
            Price = req.PriceId,Quantity = 1,string>
    {
        { "UserId",currentUser.Id },ClientReferenceId = currentUser.Id
};

我的 [HttpPost("webhook")] 现在是:

    .....
// Handle the event
if (stripeEvent.Type == Events.PaymentIntentSucceeded) //Success
{
    var paymentIntent = stripeEvent.Data.Object as PaymentIntent;
    var customerId = paymentIntent.CustomerId;
    var invoiceId = paymentIntent.InvoiceId;
    var fetchedEmail = paymentIntent.ReceiptEmail;

    var customerService = new CustomerService();
    var fetchedCustomer = customerService.Get(customerId);

    var invoiceService = new InvoiceService();
    var fetchedInvoice = invoiceService.Get(invoiceId);

    var currentUser = await _userManager.FindByEmailAsync(fetchedEmail);

    //var customerReferenceFromSession = session.Metadata.GetValueOrDefault("UserId");
    //var currentUser = await _userManager.FindByIdAsync(customerReferenceFromSession);

    var paidTimestamp = paymentIntent.Created;
    Console.WriteLine("*********************");
    Console.WriteLine(".....................");
    Console.WriteLine("Payment Intent Succeeded");
    // Then define and call a method to handle the successful payment intent.
    // handlePaymentIntentSucceeded(paymentIntent);

    var Line1 = currentUser.Street + " " + currentUser.CivicNumber;

    var paymentIntentOptions = new PaymentIntentUpdateOptions
    {
        Shipping = new ChargeShippingOptions()
        {
            Address = new AddressOptions
            {
                City = currentUser.City,PostalCode = currentUser.PostalCode.ToString(),Line1 = Line1
            },Name = currentUser.LastName + " " + currentUser.FirstName
        },};
    var paymentIntentService = new PaymentIntentService();
    paymentIntentService.Update(
    paymentIntent.Id,paymentIntentOptions
    );

    paymentIntent.ReceiptEmail = currentUser.Email;

    currentUser.hasPaidQuote = true;
    currentUser.paidOnDate = paidTimestamp;
    currentUser.SubscriptionStatus = paymentIntent.Status;
    currentUser.Status = LoginUserStatus.Approved;
    await _userManager.UpdateAsync(currentUser);

    Console.WriteLine("has paid? " + currentUser.hasPaidQuote);
    Console.WriteLine("paid on date: " + currentUser.paidOnDate.Date.ToString());
    Console.WriteLine("subscription status: " + currentUser.SubscriptionStatus);
    Console.WriteLine("user status: " + currentUser.Status.ToString());
    Console.WriteLine("customer ID: " + paymentIntent.CustomerId);
    Console.WriteLine("payment intent status: " + paymentIntent.Status);

    Console.WriteLine("invoice status from subscriptionItem: " + subscriptionItem.LatestInvoice.Status);
    Console.WriteLine("subscription status: " + subscriptionItem.Status);
    Console.WriteLine("invoice status: " + subscriptionInvoice.Paid.ToString());

    Console.WriteLine(".....................");
    Console.WriteLine("*********************");

}

它有效!! 在条带文档中做一个简短的通知会很棒。

享受编码的乐趣!