PAYATHON 2026

GWT 集成 Payment Gateway 并处理重定向

支付阿杰

明确结论

可以正常跳转,但表单必须在当前顶层窗口中提交。跳转到支付页面后,当前 GWT 页面会被卸载。支付完成并返回时,浏览器也不会“恢复”原来的 GWT module,而是重新加载 GWT 应用。

因此:

  • 不要依赖 onSubmitComplete() 处理支付结果。
  • 将表单提交到 _self 或 _top,避免支付页面出现在 GWT 的隐藏 iframe 中。
  • 将 Payment Gateway 的返回地址设置为 servlet。
  • servlet 验证并保存支付结果,再通过 HTTP 重定向返回 GWT 应用。
  • GWT 重新启动后,根据订单编号向服务端查询最终支付状态。

为什么不能依赖 onSubmitComplete()

GWT 的 FormPanel 默认通常通过隐藏 iframe 提交表单,让当前页面无需刷新。这种方式适合文件上传或普通表单请求,但不适合需要完整页面跳转的托管支付流程。

如果支付网关加载在隐藏 iframe 中,可能会遇到这些问题:

  • 用户看不到支付页面;
  • 支付网关禁止嵌入 iframe;
  • 浏览器因 X-Frame-Options 或 CSP 拒绝加载;
  • GWT 无法读取跨域响应;
  • 支付网关可能经过多次重定向,整个过程难以控制。

将支付表单提交到 _self 或 _top 后,浏览器会离开 GWT 页面。页面已经卸载,SubmitCompleteHandler 自然无法可靠地处理后续业务。SubmitHandler 仍可用于提交前禁用按钮,防止用户重复提交。

GWT 端提交表单

下面是基本结构。字段名、签名算法和 URL 必须以支付网关的接口文档为准。

FormPanel form = new FormPanel("_top");
form.setAction(paymentGatewayUrl);
form.setMethod(FormPanel.METHOD_POST);

VerticalPanel fields = new VerticalPanel();

fields.add(new Hidden("merchant_id", merchantId));
fields.add(new Hidden("order_id", orderId));
fields.add(new Hidden("amount", amount));
fields.add(new Hidden("return_url", returnUrl));
fields.add(new Hidden("signature", signature));

Button submitButton = new Button("前往支付");
fields.add(submitButton);

form.setWidget(fields);

submitButton.addClickHandler(event -> form.submit());

form.addSubmitHandler(event -> {
    submitButton.setEnabled(false);
    submitButton.setText("正在跳转…");
});

这种流程通常不需要注册 SubmitCompleteHandler:

// 对顶层页面跳转式支付流程,一般不在这里处理支付结果。
form.addSubmitCompleteHandler(event -> {
    // 不要把这里作为支付成功判定依据。
});

"_top" 会让支付页面占据整个浏览器窗口。如果应用本身没有嵌在 iframe 中,也可以使用 "_self"。

正确的重定向流程

完整流程如下:

  1. GWT 调用自己的后端创建订单。
  2. 后端生成不可重复的订单号,并保存金额、商品和用户信息。
  3. 后端按照支付网关的规则生成签名和支付参数。
  4. GWT 通过 POST 表单将这些参数提交给支付网关。
  5. 浏览器离开 GWT 页面,进入支付网关页面。
  6. 支付结束后,网关通过重定向或表单提交将浏览器带到 servlet。
  7. servlet 验证签名、订单号、金额和支付状态,并保存处理结果。
  8. servlet 将浏览器重定向回 GWT 应用。
  9. GWT module 重新加载,再根据订单号向自己的后端查询支付结果。

流程可以表示为:

GWT 页面
   └─ POST → Payment Gateway
                   └─ redirect/POST → 支付回调 servlet
                                             └─ redirect → GWT 返回页面
                                                               └─ RPC/HTTP 查询订单状态

servlet 处理后返回 GWT

支付网关的浏览器返回地址可以指向类似下面的 servlet:

@WebServlet("/payment/return")
public class PaymentReturnServlet extends HttpServlet {

    @Override
    protected void doPost(
            HttpServletRequest request,
            HttpServletResponse response) throws IOException {

        String orderId = request.getParameter("order_id");
        String paymentStatus = request.getParameter("status");
        String signature = request.getParameter("signature");

        boolean valid = verifyGatewayResponse(request);

        if (valid) {
            updatePaymentResult(orderId, paymentStatus);
        } else {
            markPaymentResponseInvalid(orderId);
        }

        String redirectUrl =
                request.getContextPath()
                + "/app.html?paymentReturn=1&orderId="
                + URLEncoder.encode(orderId, StandardCharsets.UTF_8);

        response.sendRedirect(redirectUrl);
    }
}

如果支付网关通过 GET 返回,就在 doGet() 中处理,或者让 doGet() 和 doPost() 调用同一个内部方法。具体使用哪种请求方式,必须以网关文档为准。

这里的 sendRedirect() 会让浏览器重新访问 app.html,随后 GWT module 会重新初始化。JavaScript 的执行现场无法直接“交还”给原来的 GWT module。

GWT 返回页面查询支付状态

GWT 启动后可以读取查询参数,但这些参数只用于确定需要查询哪一笔订单,不能直接用来判断支付是否成功。

public void onModuleLoad() {
    String paymentReturn =
            Window.Location.getParameter("paymentReturn");
    String orderId =
            Window.Location.getParameter("orderId");

    if ("1".equals(paymentReturn) && orderId != null) {
        showCheckingMessage();
        queryPaymentStatus(orderId);
    } else {
        showNormalApplication();
    }
}

支付状态必须从自己的服务端查询:

paymentService.getPaymentStatus(orderId, new AsyncCallback<PaymentStatus>() {
    @Override
    public void onSuccess(PaymentStatus result) {
        if (result == PaymentStatus.PAID) {
            showPaymentSuccess();
        } else if (result == PaymentStatus.PENDING) {
            showPaymentPending();
        } else {
            showPaymentFailure();
        }
    }

    @Override
    public void onFailure(Throwable caught) {
        showStatusQueryError();
    }
});

浏览器返回和服务端通知要分开

不少支付网关会提供两种回调:

  • 浏览器返回地址:支付完成后将用户带回商户网站;
  • 服务端通知地址:由支付网关直接调用商户服务器。

更新订单状态时,应优先采用经过验证的服务端通知,或者主动调用支付网关的订单查询接口。浏览器返回并不可靠。用户可能关闭页面,也可能遇到网络中断或重定向失败,导致返回请求始终没有发生。

服务端通知也可能重复发送,所以处理逻辑必须具备幂等性:

if (!paymentAlreadyProcessed(orderId, gatewayTransactionId)) {
    verifyAmountAndCurrency(orderId, request);
    markOrderAsPaid(orderId, gatewayTransactionId);
}

需要特别注意的安全问题

支付金额、商品信息和订单状态不能由 GWT 客户端决定。GWT 编译后的 JavaScript 运行在用户浏览器中,隐藏字段同样可以被修改。

至少需要做到:

  • 由服务端根据订单重新计算金额;
  • 签名和密钥只在服务端生成和保存;
  • 验证网关响应的签名;
  • 核对订单号、商户号、币种和金额;
  • 不根据 ?status=success 直接发货;
  • 防止重复通知造成重复发货;
  • 使用 HTTPS;
  • 为每次支付生成不可预测或不可伪造的订单标识;
  • 不在 GWT 页面或自己的服务器上收集信用卡信息。

原则很简单:浏览器重定向只负责页面导航,支付结果必须由服务端验证并持久化后才能确认。

备注:内容仅供参考。