使用Stripe Elements实现Razor页面订阅时遇400错误求助
我在Razor页面中用Stripe Elements实现订阅功能:已完成订阅列表页面,点击订阅可跳转至携带productId的创建订阅页面,测试信用卡(4242 4242 4242 4242)的CVC、邮编验证正常,但点击订阅按钮触发AJAX请求时出现400错误。不想使用Stripe无代码方案跳转至其官网结账,希望通过Stripe Elements解决问题(已确认密钥有效)。
CreateSubscription.CSHTML代码
@page @model myProject.Pages.Members.CreateSubscriptionsModel @{ Layout = "_member"; } <div class="container mt-5"> <h1>Create Subscription</h1> <form id="subscription-form"> @Html.AntiForgeryToken() <div id="card-element"> <!-- A Stripe Element will be inserted here. --> </div> <!-- Used to display Element errors. --> <div id="card-errors" role="alert"></div> <button id="submit-button">Subscribe</button> </form> </div> @section Scripts { <script> var stripe = Stripe('verified key'); var elements = stripe.elements(); var card = elements.create('card'); card.mount('#card-element'); card.on('change', function (event) { var displayError = document.getElementById('card-errors'); if (event.error) { displayError.textContent = event.error.message; } else { displayError.textContent = ''; } }); // Handle form submission var form = document.getElementById('subscription-form'); form.addEventListener('submit', async function (e) { e.preventDefault(); const { paymentMethod, error } = await stripe.createPaymentMethod({ type: 'card', card: card, }); if (error) var displayError = document.getElementById('card-errors'); displayError.textContent = "Payment failed: " + error.message; console.log("Payment failed: " + error.message); } else { const urlParams = new URLSearchParams(window.location.search); const planId = urlParams.get('planId'); console.log('planId:', planId); // Add this log if (!planId) { var displayError = document.getElementById('card-errors'); displayError.textContent = "Invalid URL. Missing planId."; console.log('planId:', planId); return; } const paymentMethodId = paymentMethod.id; fetch('/Members/CreateSubscriptions', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ planId, paymentMethodId }), }) .then(response => response.json()) .then(data => { if (data.status === "failure") { // Handle and display custom error var displayError = document.getElementById('card-errors'); displayError.textContent = "Subscription failed: " + data.message; } else { console.log('Success:', data); // handle successful subscription } }) .catch((error) => { console.error('Error:', error); // handle error }); } }); </script> }
后端C#代码
using myProject.Data; using Microsoft.AspNetCore.Identity; using Stripe; using Microsoft.AspNetCore.Mvc.Filters; namespace myProject.Pages.Members { public class CreateSubscriptionsModel : PageModel { public override void OnPageHandlerExecuting(PageHandlerExecutingContext context) { context.HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", "*"); base.OnPageHandlerExecuting(context); } private readonly UserManager<ApplicationUser> _userManager; public CreateSubscriptionsModel(UserManager<ApplicationUser> userManager) { _userManager = userManager; } [ValidateAntiForgeryToken] public async Task<IActionResult> OnPostAsync(string planId, string paymentMethodId) { if (string.IsNullOrEmpty(paymentMethodId)) { return new JsonResult(new { Status = "failure", Message = "Invalid payment method ID" }); } StripeConfiguration.ApiKey = "Already Verfied Key"; var planService = new PlanService(); Plan stripePlan; try { stripePlan = await planService.GetAsync(planId); } catch (StripeException e) { return new JsonResult(new { Status = "failure", Message = "Invalid plan ID: " + e.Message }); } var user = await _userManager.GetUserAsync(User); string customerId = await GetOrCreateStripeCustomerId(user); try { var options = new SubscriptionCreateOptions { Customer = customerId, Items = new List<SubscriptionItemOptions> { new SubscriptionItemOptions { Plan = planId, }, }, DefaultPaymentMethod = paymentMethodId }; var service = new SubscriptionService(); Subscription subscription = await service.CreateAsync(options); return new JsonResult(new { subscriptionId = subscription.Id }); } catch (StripeException e) { return new JsonResult(new { Status = "failure", Message = e.Message }); } } private async Task<string> GetOrCreateStripeCustomerId(ApplicationUser user) { if (!string.IsNullOrEmpty(user.StripeCustomerId)) { return user.StripeCustomerId; } var customerOptions = new CustomerCreateOptions { Email = user.Email }; var customerService = new CustomerService(); try { Customer stripeCustomer = await customerService.CreateAsync(customerOptions); user.StripeCustomerId = stripeCustomer.Id; await _userManager.UpdateAsync(user); return user.StripeCustomerId; } catch (StripeException e) { throw new Exception("Failed to create Stripe customer: " + e.Message); } } } }
错误详情
Chrome网络面板显示:请求URL为https://localhost:7141/Members/CreateSubscriptions,请求方法POST,状态码400,Referrer Policy为strict-origin-when-cross-origin。
问题排查与修复方案
1. 防伪造令牌缺失导致拦截
后端方法标注了[ValidateAntiForgeryToken],但前端AJAX请求未携带令牌,直接触发400。
修复:在fetch请求的headers中添加令牌:
fetch('/Members/CreateSubscriptions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value }, // 其余代码不变 })
2. 前端JS语法错误
if (error)代码块缺少大括号,导致逻辑执行异常:
修复:给if(error)添加大括号:
if (error) { var displayError = document.getElementById('card-errors'); displayError.textContent = "Payment failed: " + error.message; console.log("Payment failed: " + error.message); }
3. PaymentMethod未关联Customer
创建Subscription时,未将PaymentMethod附加到对应Customer会触发Stripe内部错误,表现为请求400。
修复:在后端创建Subscription前添加关联代码:
// 创建Subscription之前插入 var paymentMethodService = new PaymentMethodService(); await paymentMethodService.AttachAsync(paymentMethodId, new PaymentMethodAttachOptions { Customer = customerId });
4. JSON参数绑定失败
后端用普通参数接收JSON请求,无法正确解析。
修复:创建请求模型并使用[FromBody]绑定:
// 新增模型类 public class SubscriptionRequest { public string PlanId { get; set; } public string PaymentMethodId { get; set; } } // 修改后端方法 [ValidateAntiForgeryToken] public async Task<IActionResult> OnPostAsync([FromBody] SubscriptionRequest request) { var planId = request.PlanId; var paymentMethodId = request.PaymentMethodId; // 后续逻辑不变 }
5. 冗余跨域头设置
本地localhost请求无需设置Access-Control-Allow-Origin: *,移除OnPageHandlerExecuting中的跨域头代码即可。
内容的提问来源于stack exchange,提问作者Joticle

