如何仅使用 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 还可能重复投递,因此订单状态更新必须具备幂等性,避免重复处理同一条通知。
纯前端方案何时可用
只有支付机构明确提供以下能力时,才可能不部署自己的服务端。
托管 Payment Link
支付机构预先生成固定或可配置的付款链接,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 或托管结账方案。
备注:内容仅供参考。
