asp.net 核心 3.1 在条带 HttpPost("webhook") 中获取当前身份用户 returns NULL

asp.net core 3.1 getting current identity user within stripe HttpPost("webhook") returns NULL

我根据 stripe 的 example

在我的网站中集成了 stripe 结账支付

一切正常。我可以验证 webhooks 是否与 stripe CLI 一起工作,也可以使用 ngrok,隧道化我的 localhost.

现在我已经开始实现与身份数据库的交互。 我想在 webhook 触发 checkout.session.completed.

后将条纹 session.CustomerId 存储在那里

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

我的代码是:

[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。

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

如何让我的变量 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);
    }

在“checkout-session”中,我可以简单地检索 currentUser 没有任何问题:

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

如何在 webhook 方法中获取并 运行ning?

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

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

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,我能够找到一种方法来启动它 运行。我还在这里 and also here Reconciling "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,
        },

    },
    Metadata = new Dictionary<string, 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("*********************");

}

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

祝您编码愉快!