PAYATHON 2026

如何仅使用 JavaScript 集成支付网关与 sapui5?

支付老李

结论

如果“仅使用 JavaScript”指的是只在浏览器中的 SAPUI5 应用里编写前端代码,通常无法安全、完整地集成支付网关。

前端 JavaScript 可以处理以下工作:

  • 跳转到支付机构托管的收银台;
  • 调用支付机构提供的浏览器端 SDK;
  • 使用公开密钥采集支付信息并完成令牌化;
  • 展示支付结果。

创建支付订单、保存密钥、校验回调、查询最终支付状态和发起退款等操作,通常必须交给可信服务端。服务端可以使用 Node.js,这样整个项目仍然只使用 JavaScript,但不能只依赖浏览器端 JavaScript。

如果支付机构提供不需要密钥的 Payment Link 或托管收银台链接,也可以采用功能有限的纯前端跳转方案。

为什么不能把全部逻辑放在 SAPUI5 前端

SAPUI5 最终运行在浏览器中。前端代码会下载到用户设备,因此无法安全保存支付网关的私钥或商户密钥。下面这种写法并不安全:

const secretKey = "merchant_secret_key";

用户可以通过浏览器开发者工具、代理软件或源代码映射取得密钥。攻击者拿到密钥后,可能伪造请求、查询交易,甚至执行退款等高权限操作。

浏览器端还存在其他限制:

  • 用户可以修改请求参数;
  • 支付成功页面可以被直接访问,不能作为到账依据;
  • 支付机构的 Webhook 无法可靠地发送给浏览器;
  • 部分支付接口不允许跨域调用;
  • 用户可能在支付完成后关闭页面,导致前端收不到结果;
  • 金额、币种和订单号等关键数据不能完全信任客户端。

因此,SAPUI5 更适合负责支付流程中的界面和交互。

推荐的集成结构

较稳妥的流程如下:

SAPUI5 应用
    │
    │ 1. 请求创建支付
    ▼
JavaScript 服务端(Node.js 或 Serverless Function)
    │
    │ 2. 使用商户密钥调用支付网关
    ▼
支付网关
    │
    │ 3. 返回托管收银台 URL 或 client token
    ▼
SAPUI5 应用跳转或加载官方 SDK

支付网关
    │
    │ 4. Webhook 通知
    ▼
JavaScript 服务端验证签名并更新订单状态

这种方案不需要 Java 或 PHP。服务端可以使用:

  • Node.js;
  • SAP BTP 上的 JavaScript 服务;
  • Serverless Function;
  • 支付机构明确支持的 JavaScript 后端运行环境。

无论选择哪种方式,商户密钥都只能保存在服务端环境变量或密钥管理系统中。

SAPUI5 前端示例

下面是一个通用示例。接口地址和返回字段仅用于展示结构,需要替换为实际的服务端接口和支付网关规定的字段。

sap.ui.define([
    "sap/ui/core/mvc/Controller",
    "sap/m/MessageBox"
], function (Controller, MessageBox) {
    "use strict";

    return Controller.extend("demo.controller.Payment", {
        onPay: async function () {
            const oButton = this.byId("payButton");

            try {
                oButton.setEnabled(false);

                const oResponse = await fetch("/api/payments", {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json"
                    },
                    credentials: "same-origin",
                    body: JSON.stringify({
                        orderId: "ORDER-10001"
                    })
                });

                if (!oResponse.ok) {
                    throw new Error("创建支付失败");
                }

                const oPayment = await oResponse.json();

                if (!oPayment.checkoutUrl) {
                    throw new Error("服务端未返回支付地址");
                }

                window.location.assign(oPayment.checkoutUrl);
            } catch (oError) {
                MessageBox.error(oError.message || "暂时无法发起支付");
                oButton.setEnabled(true);
            }
        }
    });
});

对应的 SAPUI5 按钮可以这样编写:

<Button
    id="payButton"
    text="立即支付"
    type="Emphasized"
    press=".onPay" />

前端只需提交订单标识,不应把客户端传来的金额直接作为最终扣款金额。服务端应根据订单号,从可信数据源读取实际金额。

JavaScript 服务端示意

下面用 Node.js 和 Express 说明前后端的职责划分,不针对某个具体支付网关。paymentGateway.createCheckout() 代表支付机构的官方服务端 SDK 或 HTTPS API。

import express from "express";
import crypto from "node:crypto";

const app = express();

app.use(express.json());

app.post("/api/payments", async (req, res) => {
    try {
        const { orderId } = req.body;

        // 应从数据库或其他可信数据源查询,不能信任客户端提交的金额。
        const order = await findOrderById(orderId);

        if (!order || order.status !== "UNPAID") {
            return res.status(400).json({
                error: "订单不存在或当前状态不可支付"
            });
        }

        const checkout = await paymentGateway.createCheckout({
            merchantReference: order.id,
            amount: order.amount,
            currency: order.currency,
            successUrl: "https://example.com/payment/result",
            cancelUrl: "https://example.com/payment/cancel"
        });

        res.json({
            checkoutUrl: checkout.url
        });
    } catch (error) {
        res.status(500).json({
            error: "无法创建支付"
        });
    }
});

商户密钥应从服务端环境变量中读取:

const secretKey = process.env.PAYMENT_GATEWAY_SECRET_KEY;

不要把密钥写入 SAPUI5 项目的 Component.js、manifest.json、模型配置或任何会发送到浏览器的文件。

Webhook 验证示意

用户被重定向到“支付成功”页面,不代表订单已经支付成功。最终状态应以支付网关的服务端通知或服务端查询结果为准。

Webhook 通常需要使用原始请求体验证签名。具体算法、请求头名称和编码方式必须严格遵循对应支付机构的官方文档,不能直接套用下面的占位名称:

app.post(
    "/api/payment-webhook",
    express.raw({ type: "application/json" }),
    async (req, res) => {
        const signature = req.get("Payment-Signature");

        const valid = verifyGatewaySignature(
            req.body,
            signature,
            process.env.PAYMENT_WEBHOOK_SECRET
        );

        if (!valid) {
            return res.sendStatus(400);
        }

        const event = JSON.parse(req.body.toString("utf8"));

        if (event.type === "payment.succeeded") {
            await markOrderAsPaid(event.merchantReference);
        }

        res.sendStatus(200);
    }
);

Webhook 还可能重复投递,因此订单状态更新必须具备幂等性,避免重复处理同一条通知。

纯前端方案何时可用

只有支付机构明确提供以下能力时,才可能不部署自己的服务端。

支付机构预先生成固定或可配置的付款链接,SAPUI5 只负责跳转:

onPay: function () {
    window.location.assign("https://payment-provider.example/hosted-link");
}

这种方式实现简单,但未必适合动态金额、订单自动关联、库存锁定、退款和支付状态自动同步等场景。

官方浏览器端 SDK

部分支付机构允许前端使用 publishable key 获取支付令牌:

const gateway = PaymentGateway("publishable_key");

const result = await gateway.tokenize({
    paymentElement: paymentElement
});

publishable_key 可以放在前端,secret key 则不可以。生成令牌后,通常仍需由服务端创建或确认交易。能否完全在浏览器中确认交易,取决于支付机构的产品设计和官方安全要求。

支付机构承担全部订单管理

少数托管产品可以同时处理商品、金额和付款结果,应用只需打开支付页面。这类方案接近纯前端集成,但业务系统通常仍需要服务端通知机制,才能可靠地确认付款。

实施时需要注意

  • 优先使用支付机构提供的官方托管收银台或 Web SDK,不要自行采集银行卡敏感信息。
  • 不要在前端保存 secret key、Webhook secret、签名密钥或拥有商户权限的访问令牌。
  • 金额、折扣、币种和订单状态必须由可信服务端校验。
  • 不要只根据 URL 参数或成功页面把订单更新为“已支付”。
  • 为创建支付和处理 Webhook 的接口配置身份验证、签名验证、幂等控制和日志记录。
  • 检查支付机构对 HTTPS、CORS、CSP、允许域名和回调地址的要求。
  • 如果需要在 SAPUI5 中加载第三方脚本,应使用固定的官方地址,并配置相应的 Content Security Policy。
  • 涉及银行卡数据时,需要确认所选方案对应的 PCI DSS 合规范围。使用托管页面或官方支付组件,通常可以减少应用直接处理敏感数据的范围。

SAPUI5 与支付网关可以全部使用 JavaScript 技术栈,但安全、完整的方案通常是“SAPUI5 前端 JavaScript + Node.js/Serverless 服务端 JavaScript”。如果严格限定为浏览器端 JS,就应选择支付机构明确支持的 Payment Link 或托管结账方案。

备注:内容仅供参考。