PAYATHON 2026

如何在 Flash 中完成信用卡验证与支付流程?

支付小周

明确结论

Flash 可以承担结账界面,并通过 HTTPS 与自有后端通信,但支付网关密钥、信用卡验证规则和交易处理逻辑不能全部放在 SWF 中。

比较可靠的处理方式是:

  1. Flash 收集订单和支付信息,检查基本格式。
  2. Flash 通过 HTTPS POST 将请求发送到自有后端。
  3. 后端校验订单、执行风控并计算金额。
  4. 后端调用支付网关,完成令牌化、预授权或扣款。
  5. 后端将交易结果返回给 Flash。

如果支付网关要求使用 3-D Secure、银行身份验证或托管收银台,支付过程通常无法始终停留在 Flash 内。此时需要按照网关要求打开浏览器页面或其他受支持的验证界面。

Flash Player 已经结束支持,因此下面的方案仅适用于维护遗留系统。新项目应改用 HTML5,以及支付网关提供的 Hosted Fields、Payment Element 或 Checkout 页面。

为什么不能直接从 Flash 调用支付网关

SWF 文件会下载到用户电脑。经过反编译,其中的代码、接口地址和常量都可能暴露。因此,写在 Flash 中的商户密钥、API Secret 或签名私钥不能算作秘密。

直接调用支付网关还会带来以下问题:

  • 跨域请求依赖支付网关提供允许访问的 crossdomain.xml。
  • Socket 连接也受到 socket policy 限制。
  • 部分网关只接受服务器之间的请求。
  • 如果直接采用客户端提交的金额、商品和折扣,这些数据很容易被篡改。
  • 信用卡数据经过自有服务器后,PCI DSS 合规范围会明显扩大。
  • 交易超时或重复提交可能导致重复扣款。
  • 3-D Secure 等流程可能需要银行页面参与,无法用普通 POST 代替。

所以,Flash 只负责界面展示和数据提交,支付流程应由自有后端控制。

推荐的处理流程

完整的支付流程可以按以下顺序实现:

  1. 后端创建订单,并生成不可预测的 orderId。
  2. Flash 展示后端返回的商品、金额和币种。
  3. 用户填写持卡人姓名、卡号、有效期和 CVV。
  4. Flash 执行非权威性的即时检查,例如检查必填项、有效期格式和 Luhn 校验结果。
  5. Flash 将订单标识和支付数据提交到自有 HTTPS 接口。
  6. 后端重新读取订单并计算应付金额,不能信任 Flash 提交的金额。
  7. 后端调用支付网关。
  8. 网关返回成功、失败、待确认或需要额外验证等状态。
  9. 后端保存交易编号和状态,再向 Flash 返回必要的结果。
  10. 通过网关 webhook 再次确认最终交易状态。

如果网关支持客户端令牌化,而且明确兼容 Flash 或通用 HTTPS 客户端,可以先将卡号换成一次性 token,再把 token 发送给自有后端。这样可以减少信用卡数据进入业务服务器的机会。不过,PCI DSS 范围能否因此缩小,仍要根据所用网关的集成文档和合规评估确定,不能只凭”使用了 token”作出判断。

Flash 端提交示例

下面的 ActionScript 3 示例会将表单提交到同源后端,不会直接调用支付网关。

import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.events.SecurityErrorEvent;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.net.URLRequestMethod;
import flash.net.URLVariables;

function submitPayment(
    orderId:String,
    cardholder:String,
    cardNumber:String,
    expiryMonth:String,
    expiryYear:String,
    cvv:String
):void {
    if (!isValidLuhn(cardNumber)) {
        showError("卡号格式不正确");
        return;
    }

    var data:URLVariables = new URLVariables();
    data.orderId = orderId;
    data.cardholder = cardholder;
    data.cardNumber = cardNumber.replace(/\s|-/g, "");
    data.expiryMonth = expiryMonth;
    data.expiryYear = expiryYear;
    data.cvv = cvv;

    var request:URLRequest = new URLRequest(
        "https://checkout.example.com/api/payment"
    );
    request.method = URLRequestMethod.POST;
    request.data = data;
    request.contentType = "application/x-www-form-urlencoded";

    var loader:URLLoader = new URLLoader();
    loader.addEventListener(Event.COMPLETE, onPaymentComplete);
    loader.addEventListener(IOErrorEvent.IO_ERROR, onPaymentIOError);
    loader.addEventListener(
        SecurityErrorEvent.SECURITY_ERROR,
        onPaymentSecurityError
    );

    disablePayButton();
    loader.load(request);
}

function onPaymentComplete(event:Event):void {
    enablePayButton();

    var loader:URLLoader = URLLoader(event.target);
    var response:Object = JSON.parse(loader.data);

    switch (response.status) {
        case "succeeded":
            showSuccess(response.orderId);
            break;

        case "requires_action":
            handleAdditionalAuthentication(response);
            break;

        case "declined":
            showError("付款未获批准,请检查支付信息或更换银行卡");
            break;

        default:
            showError("暂时无法确认支付结果,请查询订单状态");
            break;
    }
}

function onPaymentIOError(event:IOErrorEvent):void {
    enablePayButton();
    showError("网络请求失败,请查询订单状态后再决定是否重试");
}

function onPaymentSecurityError(event:SecurityErrorEvent):void {
    enablePayButton();
    showError("安全策略阻止了支付请求");
}

function isValidLuhn(value:String):Boolean {
    var digits:String = value.replace(/\s|-/g, "");

    if (!/^\d{12,19}$/.test(digits)) {
        return false;
    }

    var sum:int = 0;
    var alternate:Boolean = false;

    for (var i:int = digits.length - 1; i >= 0; i--) {
        var n:int = int(digits.charAt(i));

        if (alternate) {
            n *= 2;
            if (n > 9) {
                n -= 9;
            }
        }

        sum += n;
        alternate = !alternate;
    }

    return sum % 10 == 0;
}

原生 JSON.parse() 要求 ActionScript 运行环境达到相应版本。较旧的 Flash Player 如果不支持原生 JSON,需要使用兼容的 JSON 库,或者让后端返回可由 URLVariables 解析的格式。

Luhn 校验只能发现部分输入错误。它无法证明银行卡真实存在,也不能代替发卡行授权。

后端处理示例

下面是与编程语言无关的伪代码。paymentGateway 代表所选支付网关的官方服务端 SDK,实际方法名应以该网关的文档为准。

POST /api/payment

require HTTPS
authenticate current checkout session
apply rate limiting

order = database.getOrder(request.orderId)

if order does not exist:
    return 404

if order belongs to another session:
    return 403

if order is already paid:
    return current order status

amount = calculateAmountFromServerSideOrder(order)
currency = order.currency

validate card field formats
validate order state
validate inventory

idempotencyKey = "payment:" + order.id

result = paymentGateway.createPayment({
    amount: amount,
    currency: currency,
    card: {
        number: request.cardNumber,
        expiryMonth: request.expiryMonth,
        expiryYear: request.expiryYear,
        cvv: request.cvv,
        holderName: request.cardholder
    },
    merchantReference: order.id,
    idempotencyKey: idempotencyKey
})

database.saveGatewayTransaction(
    order.id,
    result.transactionId,
    result.status
)

return {
    orderId: order.id,
    status: mapGatewayStatus(result.status)
}

如果条件允许,更适合的做法是让 Flash 提交支付网关生成的一次性 token:

result = paymentGateway.createPayment({
    amount: amount,
    currency: currency,
    paymentMethodToken: request.paymentMethodToken,
    merchantReference: order.id,
    idempotencyKey: idempotencyKey
})

后端返回的信息应尽可能精简。不要将支付网关的原始错误、风控规则、内部堆栈或敏感字段直接返回给客户端。

信用卡验证应分成两层

Flash 端的验证主要用于改善填写体验,可以检查:

  • 卡号是否只包含允许的字符。
  • 卡号长度是否合理。
  • Luhn 校验是否通过。
  • 月份是否在 01 到 12 之间。
  • 有效期是否已经过去。
  • CVV 是否符合基本长度要求。

后端仍须再次检查所有字段。银行卡是否有效、余额是否充足以及交易是否获批,最终由支付网关和发卡行决定,客户端验证代码无权作出这些判断。

不要只根据卡号前几位自行判断卡组织、卡种或 CVV 长度。这些规则可能发生变化,应优先使用支付网关提供的验证能力。

跨域配置

如果 SWF 和支付接口使用不同的域名,目标服务器通常需要提供 crossdomain.xml。例如:

<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy
    SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
    <site-control permitted-cross-domain-policies="master-only"/>
    <allow-access-from
        domain="checkout.example.com"
        secure="true"/>
</cross-domain-policy>

不要为了方便而配置:

<allow-access-from domain="*" secure="false"/>

跨域策略只决定 Flash Player 能否发起请求,既不提供身份验证,也无法保护 API 密钥。支付接口仍需使用 HTTPS,并执行会话校验、CSRF 防护、速率限制和服务端授权检查。

交易状态与重复扣款

支付请求超时后,不能直接提示用户”支付失败,请重新付款”。超时可能对应多种情况:

  • 请求没有到达后端。
  • 后端已经创建交易,但响应丢失。
  • 网关已经扣款,业务系统却尚未收到结果。
  • 交易仍在处理中。

每次支付都应使用稳定的幂等键。Flash 遇到超时后,应先调用订单状态接口:

GET /api/orders/{orderId}/payment-status

只有后端确认不存在已成功或正在处理的交易时,才能重新发起支付。最终状态还应根据网关 webhook 更新,不能只依赖 Flash 收到的同步响应,因为用户可能关闭窗口或在处理中途断网。

必须注意的安全与合规问题

  • 所有支付请求都必须使用有效的 HTTPS,不能降级为 HTTP。
  • 不要在 SWF 中保存 API Secret、私钥或网关管理凭据。
  • 不要记录卡号、CVV、请求正文或完整的网关响应。
  • 授权完成后不应保存 CVV。
  • 页面、接口日志、异常监控和分析系统都需要过滤支付字段。
  • 金额、币种、折扣、税费和商品信息必须由后端确定。
  • 提交支付后应暂时禁用支付按钮,但服务端的幂等控制仍然必不可少。
  • 数据库只保存业务需要的交易编号、状态和脱敏信息,例如卡号后四位。
  • 上线前应确认支付网关是否允许这种接入方式,并完成对应的 PCI DSS 合规评估。
  • 如果网关要求 3-D Secure 或其他强客户认证,必须使用其官方流程,不能通过模拟 POST 或 socket 绕过。

遗留 Flash 系统可以采用这种架构继续处理交易。如果系统仍有改造空间,更稳妥的方案是把结账界面迁移到受支持的浏览器技术,并使用支付网关托管的卡片输入组件。这样既能适应现代身份验证流程,也能减轻直接处理信用卡数据带来的安全负担。

备注:内容仅供参考。