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"。
正确的重定向流程
完整流程如下:
- GWT 调用自己的后端创建订单。
- 后端生成不可重复的订单号,并保存金额、商品和用户信息。
- 后端按照支付网关的规则生成签名和支付参数。
- GWT 通过
POST表单将这些参数提交给支付网关。 - 浏览器离开 GWT 页面,进入支付网关页面。
- 支付结束后,网关通过重定向或表单提交将浏览器带到 servlet。
- servlet 验证签名、订单号、金额和支付状态,并保存处理结果。
- servlet 将浏览器重定向回 GWT 应用。
- 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 页面或自己的服务器上收集信用卡信息。
原则很简单:浏览器重定向只负责页面导航,支付结果必须由服务端验证并持久化后才能确认。
备注:内容仅供参考。
