如何在 Adobe AIR 桌面应用中集成 payment gateway?
结论
Adobe AIR 桌面应用可以接入支付网关,但不能认为把支付页面直接嵌入应用就不会出问题。
比较稳妥的做法是:AIR 应用先向业务服务器请求创建支付订单,再通过系统默认浏览器打开支付网关提供的托管收银台。网关异步通知业务服务器支付结果,AIR 应用则向服务器查询订单的最终状态。
不要在 AIR 客户端保存商户密钥、生成支付签名,也不要只根据页面跳转结果判断支付是否成功。能否把收银台嵌入应用,还要看支付网关是否支持 WebView、Cookie、重定向、弹窗、3-D Secure 和 OAuth 类授权流程。
推荐架构
一般可以按下面的流程接入:
- AIR 应用调用业务服务器的“创建订单”接口。
- 业务服务器生成订单,并使用商户密钥请求支付网关。
- 服务器把支付页面 URL 和业务订单号返回给 AIR 应用。
- AIR 应用使用系统浏览器打开支付页面。
- 用户在浏览器中完成支付和身份验证。
- 支付网关通过 webhook 或服务端回调通知业务服务器。
- AIR 应用轮询订单查询接口,或通过其他可信渠道获取最终状态。
- 业务服务器验证回调签名,并核对金额、币种和订单号,确认无误后再交付商品或开通服务。
商户密钥和支付签名必须由服务端处理。AIR 桌面应用中的代码和配置可能被分析、提取或篡改,不适合存放这些敏感信息。
在 AIR 中打开托管支付页面
下面的 ActionScript 3 示例会请求服务端创建订单,然后使用系统默认浏览器打开支付页面:
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.net.URLRequestHeader;
import flash.net.URLRequestMethod;
import flash.net.navigateToURL;
private function createPayment():void
{
var request:URLRequest = new URLRequest(
"https://api.example.com/payments/orders"
);
request.method = URLRequestMethod.POST;
request.contentType = "application/json";
request.requestHeaders = [
new URLRequestHeader("Accept", "application/json"),
new URLRequestHeader("Authorization", "Bearer " + accessToken)
];
request.data = JSON.stringify({
productId: "desktop-pro",
quantity: 1
});
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, onOrderCreated);
loader.addEventListener(IOErrorEvent.IO_ERROR, onOrderError);
loader.load(request);
}
private function onOrderCreated(event:Event):void
{
var response:Object = JSON.parse(
URLLoader(event.currentTarget).data
);
var checkoutUrl:String = response.checkoutUrl;
currentOrderId = response.orderId;
var allowedPrefix:String = "https://payments.example-gateway.com/";
if (checkoutUrl.indexOf(allowedPrefix) != 0)
{
throw new Error("Unexpected checkout URL");
}
navigateToURL(new URLRequest(checkoutUrl), "_blank");
startPaymentStatusPolling(currentOrderId);
}
private function onOrderError(event:IOErrorEvent):void
{
trace("Unable to create payment order: " + event.text);
}
其中的域名、接口字段和身份认证方式仅用于说明整体结构,需要替换成实际业务服务器和支付网关使用的定义。生产环境不能只靠字符串前缀校验 URL,服务端也只能返回预先允许的 HTTPS 域名。
查询支付结果
AIR 应用可以定期查询自己的业务服务器,不应直接信任浏览器跳转参数:
import flash.events.TimerEvent;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.net.URLRequestHeader;
import flash.utils.Timer;
private var paymentTimer:Timer;
private var currentOrderId:String;
private function startPaymentStatusPolling(orderId:String):void
{
currentOrderId = orderId;
paymentTimer = new Timer(3000);
paymentTimer.addEventListener(
TimerEvent.TIMER,
checkPaymentStatus
);
paymentTimer.start();
}
private function checkPaymentStatus(event:TimerEvent):void
{
var request:URLRequest = new URLRequest(
"https://api.example.com/payments/orders/"
+ encodeURIComponent(currentOrderId)
);
request.requestHeaders = [
new URLRequestHeader("Accept", "application/json"),
new URLRequestHeader("Authorization", "Bearer " + accessToken)
];
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, onStatusLoaded);
loader.load(request);
}
private function onStatusLoaded(event:Event):void
{
var response:Object = JSON.parse(
URLLoader(event.currentTarget).data
);
switch (response.status)
{
case "paid":
paymentTimer.stop();
unlockPurchasedContent();
break;
case "failed":
case "cancelled":
case "expired":
paymentTimer.stop();
showPaymentFailure(response.status);
break;
case "pending":
// 继续等待服务端确认。
break;
}
}
轮询应设置超时时间和最大重试次数。用户关闭支付页面后,订单仍有可能稍后完成,所以关闭浏览器不能直接视为支付失败。
服务端回调处理
确认支付结果应以网关 webhook 为重要依据。下面是与具体框架无关的伪代码:
POST /webhooks/payment-gateway
1. 读取未经修改的原始请求体
2. 使用网关规定的方法验证 webhook 签名
3. 根据网关事件中的交易号检查事件是否已处理
4. 查询本地订单并核对:
- orderId
- amount
- currency
- merchant account
- payment status
5. 以事务方式把订单更新为 paid
6. 记录事件并返回网关要求的成功响应
支付网关可能重复投递同一个事件,因此回调处理必须具备幂等性。除了验证签名,还要核对金额、币种和订单归属。
是否应该把支付页面嵌入 AIR
从技术上看,可以尝试使用 HTMLLoader、StageWebView 或其他网页承载组件。不过,嵌入式方案有不少不确定因素:
- 支付页面可能拒绝在 iframe 或嵌入式浏览器中运行。
- 3-D Secure、银行身份验证或第三方登录可能需要弹窗和跨域跳转。
- Cookie、
SameSite、本地存储和重定向行为可能与普通浏览器不同。 - 网关可能要求使用受支持的现代浏览器环境。
- CAPTCHA、数字钱包或系统级认证可能无法正常工作。
- AIR 使用的网页渲染能力可能不满足支付页面对 JavaScript、TLS 或安全策略的要求。
- 支付网关的服务条款可能禁止在不受支持的 WebView 中收集支付信息。
除非支付网关明确支持相应的嵌入环境,而且已经在目标操作系统上完成全面测试,否则应优先使用系统浏览器。
也不要自行制作银行卡号、CVV 等敏感信息的输入字段,再由 AIR 客户端把数据提交给网关。这会明显扩大安全责任和 PCI DSS 合规范围。应优先使用支付网关托管的收银台或其官方安全组件。
返回 AIR 应用的方式
支付结束后,可以采用以下任一种方式:
- AIR 应用继续轮询服务端订单状态。
- 在支付成功页面提示用户返回桌面应用。
- 使用自定义 URL scheme 或系统支持的应用链接唤起 AIR 应用。
- 使用本地回调服务,但需要处理端口占用、伪造请求和防火墙问题。
自定义 URL scheme 只能改善操作体验,不能单独用来证明支付成功。攻击者可以手动构造类似下面的地址:
myapp://payment-complete?orderId=123
AIR 应用收到该地址后,仍需向业务服务器查询订单状态。不能因为 URL 中带有 success=true 就直接交付商品。
需要重点检查的事项
上线前至少要确认以下事项:
- 支付网关是否允许桌面应用使用系统浏览器完成支付。
- 必须使用官方 SDK,还是可以使用托管收银台。
- Windows 和 macOS 上的浏览器跳转是否正常。
- 3-D Secure、取消支付、超时和支付后返回流程是否正常。
- 网关 webhook 是否正确验证签名并防止重放。
- 是否妥善处理同一订单重复支付、重复回调和网络中断。
- 金额和商品信息是否由服务端计算,客户端是否只能提交商品标识。
- 所有接口是否使用 HTTPS,并正确验证证书。
- 访问令牌是否短期有效,日志是否排除了密钥和支付敏感信息。
- 支付成功后是否由服务端签发授权,而不是只修改 AIR 本地数据。
最终建议
Adobe AIR 本身不会阻止应用接入支付网关。实际限制通常来自网关的接入规则、嵌入式网页的兼容性,以及安全合规要求。
对大多数桌面应用来说,更可靠的流程是“服务端创建订单 + 系统浏览器托管支付 + webhook 确认 + AIR 查询结果”。只有支付网关明确支持 AIR 使用的网页容器,并且该方案通过了跨平台验证,才适合考虑直接在应用窗口内展示支付页面。
备注:内容仅供参考。
