使用 JQuery/AJAX 处理支付是否存在安全风险?
明确结论
用 jQuery/AJAX 提交支付信息,并不比普通 HTML 表单提交更不安全。两种方式最终都会通过 HTTP 请求传输数据。是否安全,取决于传输加密、支付数据的接触范围、服务端验证和密钥管理,与是否使用 AJAX 无关。
需要特别注意的是,如果银行卡号、CVV 或银行账户信息先发送到自己的服务器,网站就会直接处理敏感支付数据,安全风险和 PCI DSS 合规成本也会明显增加。
更稳妥的做法是使用支付服务商提供的托管支付页、嵌入式安全组件或前端 SDK,让浏览器直接将支付信息提交给支付服务商。自己的服务器只接收 token、Payment Method ID 等非原始支付凭据。
推荐的处理流程
常见的处理流程是:
- 用户在支付服务商提供的安全输入组件中填写卡号或银行账户信息。
- 支付服务商的前端 SDK 直接收集这些信息。
- SDK 返回一次性 token 或支付方式标识。
- 浏览器通过 AJAX 将该标识和订单号发送到自己的服务器。
- 服务器重新查询订单金额,再使用私钥调用支付服务商 API。
- 服务端结合 API 响应和 webhook 确认支付结果。
不要直接采用浏览器提交的最终金额进行扣款。价格、币种、折扣、税费和订单状态都要由服务端根据可信数据重新计算。
前端示例
下面是通用示意代码。paymentProvider.createToken() 是占位名称,实际使用时应替换为所选支付服务商 SDK 提供的方法。
<form id="payment-form">
<!-- 这里应挂载支付服务商提供的安全输入组件 -->
<div id="payment-element"></div>
<button type="submit" id="pay-button">确认支付</button>
</form>
$("#payment-form").on("submit", async function (event) {
event.preventDefault();
const button = $("#pay-button");
button.prop("disabled", true);
try {
// 原始卡号应由支付服务商 SDK 读取,不要自行从 input 中取出。
const tokenResult = await paymentProvider.createToken();
if (tokenResult.error) {
throw new Error(tokenResult.error.message);
}
const response = await $.ajax({
url: "/api/payments",
method: "POST",
contentType: "application/json",
headers: {
"X-CSRF-Token": window.csrfToken
},
data: JSON.stringify({
orderId: "ORDER_ID",
paymentToken: tokenResult.token
})
});
// 前端显示结果,但订单是否支付成功应以服务端确认结果为准。
console.log(response);
} catch (error) {
console.error("Payment failed:", error.message);
} finally {
button.prop("disabled", false);
}
});
这里最重要的是,AJAX 请求传输的是支付 token,而不是卡号、CVV、有效期或银行账号。
服务端必须做的验证
下面的 Node.js/Express 示例也是通用结构。支付服务商的 API 名称需要根据实际 SDK 调整:
app.post("/api/payments", requireLogin, verifyCsrfToken, async (req, res) => {
const { orderId, paymentToken } = req.body;
if (typeof orderId !== "string" ||
typeof paymentToken !== "string" ||
!paymentToken) {
return res.status(400).json({ error: "Invalid request" });
}
// 必须从数据库读取订单,不能采用浏览器提交的金额。
const order = await db.orders.findById(orderId);
if (!order || order.userId !== req.user.id) {
return res.status(404).json({ error: "Order not found" });
}
if (order.status !== "pending") {
return res.status(409).json({ error: "Order cannot be paid" });
}
try {
const payment = await paymentProvider.payments.create({
amount: order.totalAmount,
currency: order.currency,
paymentToken,
reference: order.id,
idempotencyKey: `payment:${order.id}`
});
// 不要仅凭前端返回值更新订单。还应验证支付服务商的 webhook。
await db.payments.save({
orderId: order.id,
providerPaymentId: payment.id,
status: payment.status
});
return res.json({
paymentId: payment.id,
status: payment.status
});
} catch (error) {
// 日志中不要写入 token、卡号、CVV 或银行账户信息。
console.error("Payment provider request failed", {
orderId: order.id,
errorCode: error.code
});
return res.status(502).json({ error: "Payment could not be processed" });
}
});
idempotencyKey 可以降低因重复点击、网络重试或超时而重复扣款的风险。具体字段名和处理方式由支付服务商决定,不能假设所有 API 都采用相同的实现。
必须采取的安全措施
全程使用 HTTPS
支付页面、AJAX 接口、重定向地址和 webhook 地址都要使用 HTTPS。还应启用 HSTS,并禁止 HTTP 页面加载支付脚本。
HTTPS 可以保护传输过程中的数据,但无法防止 XSS、服务器入侵或恶意第三方脚本。
尽量不接触原始支付信息
优先考虑以下方案:
- 支付服务商托管的结账页面;
- iframe 或安全支付组件;
- 官方前端 tokenization SDK;
- Apple Pay、Google Pay 等支付服务商支持的钱包方案。
不要将原始卡号、CVV 或银行账户信息写入数据库、日志、分析平台、错误追踪系统、浏览器存储或 URL。
CVV 通常不能在授权完成后保存。具体义务应以适用的 PCI DSS 要求、收单机构规则和支付服务商文档为准。
防止 XSS 和第三方脚本窃取数据
支付页面中的 XSS 可以在数据加密前读取用户输入,所以只有 HTTPS 还不够。还需要:
- 根据内容所在的上下文进行转义;
- 避免使用
innerHTML拼接不可信内容; - 设置严格的 Content Security Policy;
- 限制支付页面加载的第三方脚本;
- 定期检查依赖和前端资源是否遭到篡改;
- 不在支付页面部署不必要的广告、客服或分析脚本。
如果必须加载外部静态资源,可以在适用时使用 Subresource Integrity。动态支付 SDK 是否支持固定完整性哈希,则要查看服务商的加载要求。
防止 CSRF 和越权支付
支付接口需要验证用户身份、会话状态和 CSRF token。服务端还要确认:
- 当前用户有权支付该订单;
- 订单仍处于可支付状态;
- 金额和币种来自服务端;
- 优惠和税费已经重新计算;
- 同一订单不会被重复扣款。
CORS 不能代替 CSRF 防护。即使已经配置 CORS,服务端仍然需要验证请求身份和业务权限。
妥善管理密钥
支付服务商的私钥只能保存在服务端,不能写入 JavaScript、HTML、移动端包或公开仓库。
前端可以使用服务商明确标记为可公开的 publishable key。生产环境和测试环境的密钥要分开管理,并设置轮换机制、最小权限和访问审计。
验证 webhook
不能因为浏览器跳转到”支付成功”页面,就认定款项已经到账。浏览器响应可能被篡改,用户也可能在支付完成前关闭页面。
处理 webhook 时需要:
- 验证签名;
- 检查时间戳或重放保护机制;
- 根据支付服务商的事件 ID 去重;
- 核对订单号、金额、币种和商户账户;
- 确保重复事件能够安全处理;
- 不信任请求体中没有签名保护的字段。
签名算法、原始请求体要求和允许的时间范围,都必须按照对应支付服务商的官方文档实现。
控制错误信息和日志
返回给浏览器的错误信息要便于用户理解,但不能暴露内部堆栈、数据库信息或风控细节。
日志中要过滤以下内容:
- 卡号和安全码;
- 银行账号和路由号码;
- 身份证件信息;
- 支付 token;
Authorization请求头;- 完整 webhook 请求体中的敏感字段。
token 通常不能直接等同于卡号,但这不意味着它适合公开记录。
ECheck 的额外注意事项
ECheck 或 ACH 类支付可能涉及银行账号、账户持有人授权、退票和延迟结算。具体要求取决于所在国家或地区、支付网络以及服务商规则。
除了基本安全措施,通常还要注意:
- 使用支付服务商提供的银行账户验证流程;
- 保存合法的授权记录,不保存原始账户凭据;
- 明确展示扣款金额、日期和撤销规则;
- 正确处理
pending、returned、failed等异步状态; - 在最终结算前,不要将订单永久标记为已付款。
常见误区
“AJAX 请求在开发者工具中可见”并不表示 HTTPS 失效。用户本来就可以查看自己浏览器发出的请求,不能将前端代码、隐藏字段或代码混淆当作安全边界。
以下做法同样不能保护支付安全:
- 禁用右键或开发者工具;
- 在 JavaScript 中加密,再将解密密钥放在前端;
- 使用 Base64 编码;
- 将金额写在隐藏字段中;
- 只验证前端表单;
- 仅凭 AJAX 返回的
success更新订单。
建议
可以用 jQuery AJAX 完成支付流程,但 AJAX 请求最好只传递订单标识和支付服务商生成的 token。原始银行卡或银行账户信息应尽量直接进入支付服务商的受控环境。
如果业务确实要求自己的服务器接收或保存原始支付数据,就需要实施更严格的系统隔离、访问控制、审计和漏洞管理,并进行 PCI DSS 合规评估。除非有明确的业务需要和专业安全团队支持,否则不建议自行建设这部分基础设施。
备注:内容仅供参考。
