PAYATHON 2026

如何在 Adobe AIR 桌面应用中集成 payment gateway?

支付老李

结论

Adobe AIR 桌面应用可以接入支付网关,但不能认为把支付页面直接嵌入应用就不会出问题。

比较稳妥的做法是:AIR 应用先向业务服务器请求创建支付订单,再通过系统默认浏览器打开支付网关提供的托管收银台。网关异步通知业务服务器支付结果,AIR 应用则向服务器查询订单的最终状态。

不要在 AIR 客户端保存商户密钥、生成支付签名,也不要只根据页面跳转结果判断支付是否成功。能否把收银台嵌入应用,还要看支付网关是否支持 WebView、Cookie、重定向、弹窗、3-D Secure 和 OAuth 类授权流程。

推荐架构

一般可以按下面的流程接入:

  1. AIR 应用调用业务服务器的“创建订单”接口。
  2. 业务服务器生成订单,并使用商户密钥请求支付网关。
  3. 服务器把支付页面 URL 和业务订单号返回给 AIR 应用。
  4. AIR 应用使用系统浏览器打开支付页面。
  5. 用户在浏览器中完成支付和身份验证。
  6. 支付网关通过 webhook 或服务端回调通知业务服务器。
  7. AIR 应用轮询订单查询接口,或通过其他可信渠道获取最终状态。
  8. 业务服务器验证回调签名,并核对金额、币种和订单号,确认无误后再交付商品或开通服务。

商户密钥和支付签名必须由服务端处理。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 使用的网页容器,并且该方案通过了跨平台验证,才适合考虑直接在应用窗口内展示支付页面。

备注:内容仅供参考。