如何在 PhoneGap 应用中实现 Payment gateway?
结论
PhoneGap 应用可以接入支付网关,但支付密钥、可信订单的生成和支付结果的确认都不应放在应用端处理。建议采用以下流程:
- PhoneGap 请求业务后端创建订单。
- 后端调用支付网关 API,创建支付会话或生成支付参数。
- 应用通过支付网关官方 SDK、PhoneGap/Cordova 插件或系统浏览器完成付款。
- 支付网关通过服务端回调通知后端。
- 后端验签并查询支付状态,确认付款成功后再更新订单。
代码和参数需要根据支付服务商调整。不同网关可能使用 Hosted Checkout、原生 SDK 或浏览器跳转等接入方式。
为什么需要服务端
PhoneGap 本质上是运行在 WebView 中的 HTML、CSS 和 JavaScript 应用。安装包内的 JavaScript 可以被查看和分析,因此不能在应用中保存商户密钥、API Secret、签名私钥等敏感信息。
例如,下面的代码并不安全:
const secretKey = "YOUR_PAYMENT_SECRET";
攻击者取得密钥后,可能伪造请求或操作商户接口。客户端提交的支付金额同样不能直接信任,后端需要根据商品编号、优惠规则和用户订单重新计算实际金额。
推荐的接入流程
1. 选择支付网关及支付方式
先确认支付服务商是否提供以下能力中的一种或多种:
- 托管收银台,即 Hosted Checkout
- 适用于 Cordova/PhoneGap 的官方插件
- Android 和 iOS 原生 SDK
- 支持移动端跳转的网页支付
- 服务端订单查询、退款和异步通知接口
如果支付服务商没有维护 PhoneGap 插件,可以优先考虑托管收银台。这样可以减少应用直接接触银行卡数据的范围,维护成本通常也低于自行封装原生 SDK。
除非团队具备相应的支付安全和合规能力,否则不要在普通 WebView 中自行制作银行卡表单,再将卡号发送到自己的服务器。
2. 由应用请求后端创建订单
PhoneGap 只向业务后端提交商品和订单信息,不提交商户密钥。
async function createPayment(productId) {
const response = await fetch("https://api.example.com/payments", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + getAccessToken()
},
body: JSON.stringify({
productId: productId,
quantity: 1
})
});
if (!response.ok) {
throw new Error("创建支付订单失败");
}
return response.json();
}
后端需要完成以下工作:
- 验证登录用户
- 检查商品是否有效
- 根据服务端数据计算金额
- 创建唯一的商户订单号
- 调用支付网关创建支付会话
- 保存商户订单号与网关订单号的对应关系
- 向应用返回支付地址或客户端令牌
最终支付金额不能由应用自行决定。
3. 后端调用支付网关
下面是一个抽象的 Node.js 示例。接口路径、字段名称和签名方式仅用于说明整体结构,接入时必须替换为支付网关官方 API 中的实际配置。
import express from "express";
const app = express();
app.use(express.json());
app.post("/payments", async (req, res) => {
const user = await authenticate(req);
const product = await findProduct(req.body.productId);
if (!user || !product) {
return res.status(400).json({ error: "Invalid request" });
}
const order = await createLocalOrder({
userId: user.id,
productId: product.id,
amount: product.price,
currency: "CNY",
status: "PENDING"
});
const gatewayResponse = await fetch(
"https://payment-provider.example/api/checkout-sessions",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.PAYMENT_SECRET}`
},
body: JSON.stringify({
orderId: order.id,
amount: order.amount,
currency: order.currency,
returnUrl: "myapp://payment/return",
notifyUrl: "https://api.example.com/payment-webhook"
})
}
);
if (!gatewayResponse.ok) {
return res.status(502).json({ error: "Payment gateway error" });
}
const session = await gatewayResponse.json();
await saveGatewayOrderId(order.id, session.id);
res.json({
orderId: order.id,
checkoutUrl: session.checkoutUrl
});
});
生产环境还要处理超时、重试、日志脱敏和幂等请求。密钥应保存在服务器的环境变量或密钥管理系统中。
4. 在应用中打开支付页面
如果支付网关提供托管收银台,可以通过系统浏览器或受支持的 Cordova 插件打开支付地址。与任意内嵌 WebView 相比,系统浏览器通常更适合处理登录、3-D Secure 和第三方应用跳转。
下面以 cordova-plugin-inappbrowser 的调用方式为例:
async function startPayment(productId) {
try {
const payment = await createPayment(productId);
const browser = cordova.InAppBrowser.open(
payment.checkoutUrl,
"_system",
"location=yes"
);
return {
orderId: payment.orderId,
browser: browser
};
} catch (error) {
console.error(error);
alert("暂时无法发起支付,请稍后重试");
}
}
插件名称和可用参数可能因项目环境而异,应以所选插件和支付服务商的当前文档为准。如果支付页面禁止嵌入,或者需要跳转到外部支付应用,应使用 _system 在系统浏览器中打开。
5. 配置支付完成后的返回地址
支付结束后,可以通过自定义 URL Scheme 或 Universal Links/App Links 返回应用,例如:
myapp://payment/return?orderId=ORDER_ID
应用收到返回链接后,不能直接根据查询参数中的 status=success 判定支付成功。返回链接可能被伪造,用户关闭页面、网络中断或跳转失败也会使客户端状态变得不可靠。
应用应使用订单号向自己的后端查询:
async function queryPaymentStatus(orderId) {
const response = await fetch(
`https://api.example.com/payments/${encodeURIComponent(orderId)}`,
{
headers: {
"Authorization": "Bearer " + getAccessToken()
}
}
);
if (!response.ok) {
throw new Error("查询支付状态失败");
}
return response.json();
}
服务端回调与验签
支付网关通常会向 notifyUrl 发送异步通知。后端收到通知后,需要按照网关规则验证签名,并检查以下内容:
- 商户订单号是否存在
- 网关订单号是否匹配
- 商户号是否正确
- 支付金额和币种是否一致
- 通知是否已经处理
- 网关返回的交易状态是否有效
示意代码如下:
app.post("/payment-webhook", express.raw({ type: "*/*" }), async (req, res) => {
const signature = req.headers["x-payment-signature"];
const verified = verifyGatewaySignature(
req.body,
signature,
process.env.PAYMENT_WEBHOOK_SECRET
);
if (!verified) {
return res.status(400).send("invalid signature");
}
const event = parseGatewayEvent(req.body);
if (event.type === "payment.succeeded") {
await markOrderPaidOnce({
orderId: event.orderId,
gatewayTransactionId: event.transactionId,
amount: event.amount,
currency: event.currency
});
}
res.status(200).send("ok");
});
verifyGatewaySignature、事件名称和请求头都要按照实际支付网关实现。部分网关还建议后端收到通知后,主动调用订单查询接口再次确认。
订单更新必须具有幂等性。同一条成功通知即使多次到达,也只能触发一次发货、充值或权益发放。
原生 SDK 的接入方式
如果支付服务商只提供 Android 和 iOS SDK,可以采用以下方式之一:
- 使用支付服务商官方 Cordova 插件
- 使用维护可靠的第三方 Cordova 插件
- 编写自定义 Cordova 插件,分别封装 Android 和 iOS SDK
JavaScript 层通常只负责调用插件:
cordova.exec(
function onSuccess(result) {
console.log("支付流程已返回", result);
queryPaymentStatus(currentOrderId);
},
function onError(error) {
console.error("支付调用失败", error);
},
"PaymentPlugin",
"pay",
[paymentParameters]
);
即使原生 SDK 返回”成功”,应用仍要向后端查询最终状态。插件回调只能表示客户端支付流程已经返回,不能代替支付网关的服务端通知和验签。
常见问题
跨域请求失败
PhoneGap 请求业务后端时,可能受到 Content Security Policy、Cordova 白名单配置或服务端 CORS 策略的影响。配置时应只允许可信域名,并在服务端正确设置允许的请求来源、方法和请求头。
不要为了方便而允许所有来源访问带有用户凭证的接口。
支付完成后没有回到应用
需要检查以下配置:
- 自定义 URL Scheme 是否正确注册
- Android App Links 或 iOS Universal Links 是否配置完整
- 支付网关是否允许所填写的返回地址
- 应用能否正确处理冷启动和后台唤醒场景
即使没有自动返回,用户重新进入订单页后,也应该能够查询到真实的支付状态。
用户重复点击支付按钮
客户端可以暂时禁用按钮,后端则要通过幂等键或复用未支付订单,避免重复创建订单:
let paying = false;
async function handlePay(productId) {
if (paying) {
return;
}
paying = true;
try {
await startPayment(productId);
} finally {
paying = false;
}
}
客户端防重复只能改善操作体验。真正的重复订单防护仍要由后端完成。
安全与上线注意事项
- 应用与后端、后端与网关之间的通信都应使用 HTTPS。
- 不要在 PhoneGap 代码、配置文件或安装包中保存支付密钥。
- 不要信任客户端提交的金额、折扣、币种和支付状态。
- 回调接口必须验签,同时校验订单金额和币种。
- 日志中不要记录卡号、安全码、完整令牌或签名密钥。
- 分别处理支付成功、失败、取消、处理中和超时状态。
- 先在支付网关提供的沙箱环境中联调,再切换到生产配置。
- 明确隔离沙箱与生产环境的密钥、接口地址和回调地址。
- 退款应由后端调用网关接口,并保存退款记录。
- 涉及银行卡数据时,需要评估 PCI DSS 等合规要求。具体义务取决于业务地区、支付方式和接入模式。
PhoneGap 负责发起支付和展示结果,业务后端负责订单、金额、密钥、验签和最终状态。守住这一边界后,接入其他支付网关时,通常只需替换后端的网关调用和客户端的支付入口。
备注:内容仅供参考。
