React Router Loader实现Stripe嵌入结账返回401错误求助
Stripe嵌入式结账表单401未授权错误排查
问题详情
按照Stripe官方文档实现Node/Express+React的嵌入式结账表单,前端改用React Router v6的loader替代示例中的useEffect。后端创建支付会话的接口返回200,前端能获取到有效clientSecret,但加载嵌入表单时,https://api.stripe.com/v1/payment_pages/ 端点返回401(未授权)错误,而Stripe日志中仅显示200响应,无401相关记录。已确认测试API密钥正确,求排查问题原因。
后端代码
const stripe = require('stripe')(`${process.env.STRIPE_SECRET_KEY}`); router.post('/create-payment-session', requireLogin, async (req, res) => { try { const orderId = req.query.order_id; const orderData = await db.getOrderById(orderId); // Generate checkout session Price objects (line items) const orderItemsData = orderData.order_items.map(item => { return { price_data: { currency: 'usd', product_data: { name: item.product_name, }, unit_amount: (Number(item.product_price.substring(1)) * 100), // Price in cents }, quantity: Number(item.product_quantity), } }); const session = await stripe.checkout.sessions.create({ ui_mode: 'embedded', line_items: orderItemsData, mode: 'payment', return_url: `${process.env.FRONT_END_BASE_URL}/checkout/${orderId}/payment-return?session_id={CHECKOUT_SESSION_ID}`, }); res.send({clientSecret: session.client_secret}); } catch(err) { console.log(err); res.status(500).send(); } }); router.get('/payment-session-status', requireLogin, async (req, res) => { const session = await stripe.checkout.sessions.retrieve(req.query.session_id); res.send({ status: session.status }); });
前端代码
import { useLoaderData, useRouteLoaderData } from "react-router-dom"; import { loadStripe } from "@stripe/stripe-js"; import { EmbeddedCheckoutProvider, EmbeddedCheckout } from "@stripe/react-stripe-js"; import InlineErrorPage from "../../components/InlineErrorPage/InlineErrorPage"; const stripePromise = loadStripe(`${process.env.REACT_APP_STRIPE_PUBLIC_KEY}`); export async function paymentSessionLoader({ params }) { try { // Create a Checkout Session as soon as the page loads const baseEndpoint = `${process.env.REACT_APP_API_BASE_URL}/checkout/create-payment-session`; const res = await fetch(`${baseEndpoint}?order_id=${params.orderId}`, { method: "POST", credentials: "include", }); console.log(res); if (res.ok) { const data = await res.json(); return { clientSecret: data.clientSecret }; } throw new Error("Unexpected status code."); } catch (error) { console.log(error); return { error: "Sorry, payment is not currently possible. Please try again later." }; } } export function PaymentPage() { const authData = useRouteLoaderData("app"); const { clientSecret, error } = useLoaderData(); console.log(clientSecret); if (!authData.logged_in) { return <InlineErrorPage pageName="Order details" type="login_required" loginRedirect="/orders" />; } else if (error) { return <InlineErrorPage pageName="Order failed" message={error} />; } return ( <div id="checkout"> {clientSecret && ( <EmbeddedCheckoutProvider stripe={stripePromise} options={{clientSecret}} > <EmbeddedCheckout /> </EmbeddedCheckoutProvider> )} </div> ) }
排查方向
- 确认clientSecret完整性:对比前端控制台打印的clientSecret与后端返回的
session.client_secret,确保没有截断、空格或字符错误。 - 验证密钥环境一致性:检查前端
REACT_APP_STRIPE_PUBLIC_KEY是否为测试环境公钥(以pk_test开头),且与后端使用的测试密钥(以sk_test开头)属于同一Stripe账号。 - 检查CORS配置:后端需允许前端域名发起跨域请求,若CORS配置缺失或错误,可能导致Stripe嵌入式表单的请求授权信息传递异常。
- 排查请求拦截/代理干扰:检查前端是否有全局请求拦截器,或使用了代理工具(如Webpack Dev Server代理),这类工具可能修改Stripe请求的头信息,导致授权失败。
- 核对Checkout Session配置:确认创建会话时
ui_mode: 'embedded'设置正确,return_url为HTTPS且属于Stripe后台已配置的域名(测试环境可暂时跳过域名验证,但正式环境必须配置)。 - 隔离测试会话创建流程:直接在前端使用Stripe测试密钥创建简单的Checkout Session,拿到clientSecret后初始化表单,验证是否仍出现401,以此排除后端会话创建的问题。
- 分析浏览器请求头:在浏览器开发者工具的Network面板中,查看
payment_pages请求的Authorization头,确认其格式为Bearer {你的公钥},且公钥正确。若授权头缺失或错误,会直接导致401。
内容的提问来源于stack exchange,提问作者Cmastris
相关产品推荐
相关产品推荐

