PAYATHON 2026

使用 JQuery/AJAX 处理支付是否存在安全风险?

支付小周

明确结论

用 jQuery/AJAX 提交支付信息,并不比普通 HTML 表单提交更不安全。两种方式最终都会通过 HTTP 请求传输数据。是否安全,取决于传输加密、支付数据的接触范围、服务端验证和密钥管理,与是否使用 AJAX 无关。

需要特别注意的是,如果银行卡号、CVV 或银行账户信息先发送到自己的服务器,网站就会直接处理敏感支付数据,安全风险和 PCI DSS 合规成本也会明显增加。

更稳妥的做法是使用支付服务商提供的托管支付页、嵌入式安全组件或前端 SDK,让浏览器直接将支付信息提交给支付服务商。自己的服务器只接收 token、Payment Method ID 等非原始支付凭据。

推荐的处理流程

常见的处理流程是:

  1. 用户在支付服务商提供的安全输入组件中填写卡号或银行账户信息。
  2. 支付服务商的前端 SDK 直接收集这些信息。
  3. SDK 返回一次性 token 或支付方式标识。
  4. 浏览器通过 AJAX 将该标识和订单号发送到自己的服务器。
  5. 服务器重新查询订单金额,再使用私钥调用支付服务商 API。
  6. 服务端结合 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 等异步状态;
  • 在最终结算前,不要将订单永久标记为已付款。

常见误区

“A‌JAX 请求在开发者工具中可见”并不表示 HTTPS 失效。用户本来就可以查看自己浏览器发出的请求,不能将前端代码、隐藏字段或代码混淆当作安全边界。

以下做法同样不能保护支付安全:

  • 禁用右键或开发者工具;
  • 在 JavaScript 中加密,再将解密密钥放在前端;
  • 使用 Base64 编码;
  • 将金额写在隐藏字段中;
  • 只验证前端表单;
  • 仅凭 AJAX 返回的 success 更新订单。

建议

可以用 jQuery AJAX 完成支付流程,但 AJAX 请求最好只传递订单标识和支付服务商生成的 token。原始银行卡或银行账户信息应尽量直接进入支付服务商的受控环境。

如果业务确实要求自己的服务器接收或保存原始支付数据,就需要实施更严格的系统隔离、访问控制、审计和漏洞管理,并进行 PCI DSS 合规评估。除非有明确的业务需要和专业安全团队支持,否则不建议自行建设这部分基础设施。

备注:内容仅供参考。