如何在Stripe Elements支付流程中正确集成CAPTCHA防范卡测试
解决方案
纯前端校验CAPTCHA没有任何防攻击价值,攻击者完全可以绕过前端判断直接发起支付请求,你不需要把stripe.confirmCardPayment迁移到后端就能实现安全的CAPTCHA集成,整个流程完全兼容3D Secure验证逻辑,核心是把CAPTCHA的有效性校验放到你已有的后端API路由里执行。
调整后的流程逻辑
- 用户点击提交支付时,前端先触发reCAPTCHA组件获取用户校验令牌,不在前端做任何有效性判断
- 前端调用已有的
/api/payment-intent/update接口时,把CAPTCHA令牌作为参数一起传给后端 - 后端收到请求后,第一时间用服务端密钥校验CAPTCHA令牌的真实性:
- 校验不通过直接返回403错误,终止所有后续逻辑,前端收到错误直接提示,不会走到支付确认步骤
- 校验通过再执行原本的支付意图更新逻辑,返回成功响应
- 前端收到后端更新成功的响应后,完全沿用原本的
stripe.confirmCardPayment逻辑,不影响3D验证弹窗等原生能力
代码调整示例
前端提交逻辑修改
// 页面初始化时加载reCAPTCHA脚本,以下以v3版本为例 const getRecaptchaToken = async () => { return await window.grecaptcha.execute(process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY, { action: 'one_off_donation' }); }; export const makeOneOffPayment = async ( e, stripe, elements, clientSecret, setError, billingInfo, paymentAmount, paymentIntentId, CardNumberElement, ): Promise<boolean> => { e.preventDefault(); // 获取CAPTCHA令牌,不做本地校验 let recaptchaToken; try { recaptchaToken = await getRecaptchaToken(); } catch (err) { setError('安全校验失败,请刷新页面重试'); return false; } // 更新支付意图时携带CAPTCHA令牌 try { const res = await fetch('/api/payment-intent/update', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: paymentAmount * 100, paymentIntentId: paymentIntentId, recaptchaToken }), }); if (!(res.status >= 200 && res.status <= 299)) { const result = await res.json(); setError(`支付失败:${result.message}`); return false; } } catch (err) { setError('客户端处理异常,请重试'); return false; } // 后端校验通过后再执行卡支付确认,原有逻辑完全保留,支持3D验证 try { const payload = await stripe.confirmCardPayment(clientSecret, { payment_method: { card: elements.getElement(CardNumberElement), billing_details: { address: { city: billingInfo.city || '', line1: billingInfo.AddressLine1 || '', line2: billingInfo.AddressLine2 || '', }, email: billingInfo.email, name: billingInfo.name, phone: billingInfo.phone, }, }, }); if (payload.error) { setError(`支付失败:${payload.error.message}`); return false; } return true; } catch (err) { setError('客户端处理异常,请重试'); return false; } };
后端API路由修改
在你已有的/api/payment-intent/update接口里增加CAPTCHA服务端校验逻辑:
import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: '请求方法不允许' }); } const { amount, paymentIntentId, recaptchaToken } = req.body; // 服务端校验CAPTCHA,密钥存在环境变量中,前端无法获取 const captchaVerifyRes = await fetch('RECAPTCHA官方服务端校验地址', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ secret: process.env.RECAPTCHA_SECRET_KEY, response: recaptchaToken, remoteip: req.headers['x-forwarded-for']?.split(',')[0] || req.socket.remoteAddress }) }); const captchaResult = await captchaVerifyRes.json(); // 校验规则可根据需求调整:验证成功、分数达标、动作匹配 if ( !captchaResult.success || captchaResult.score < 0.5 || captchaResult.action !== 'one_off_donation' ) { return res.status(403).json({ message: '安全校验未通过,请稍后重试' }); } // 校验通过后执行原有支付意图更新逻辑 try { await stripe.paymentIntents.update(paymentIntentId, { amount }); return res.status(200).json({ success: true }); } catch (err) { return res.status(400).json({ message: err.message }); } }
额外防卡测试优化建议
- 前端不要返回精准的卡错误信息,比如不要单独提示“CVV错误”“卡号不存在”“余额不足”,统一返回“支付失败,请检查卡信息后重试”,减少攻击者试卡的信息反馈
- 开启支付接口的频率限制,单IP1分钟内最多允许3次支付提交,超出直接拦截
- 配合Stripe自带的Radar防欺诈规则,对短时间内多次支付失败的IP、卡号段做自动拦截
- 如果用reCAPTCHA v3,对分数在0.3-0.7之间的请求弹出滑块验证,进一步提高攻击成本
内容的提问来源于stack exchange,提问作者David Fox
相关产品推荐
相关产品推荐

