payment gateway 成功支付后 session 返回 null
结论
request.getSession(false) 返回 null,说明当前支付回调请求没有携带可识别的 Session,或者原 Session 已经失效。
支付网关的回调通常由网关服务器直接发起,并非来自用户浏览器,因此一般不会携带应用的 JSESSIONID。
支付入库不应依赖 Session。跳转到支付网关前,先创建本地订单,再用商户订单号关联支付请求和回调。收到回调后,需要验证签名、金额及支付状态,并以幂等方式更新订单。
Session 为什么会丢失
request.getSession(false) 只获取已经存在且有效的 Session,不会创建新 Session。以下情况都可能返回 null:
- 支付网关通过服务器间通知访问回调地址,请求中没有用户浏览器的 Cookie。
- 用户在支付页面停留过久,原 Session 已过期。
- 回调使用了不同的域名、协议或端口,浏览器没有发送原来的
JSESSIONID。 - Cookie 的
SameSite、Secure或Domain配置阻止了跨站回跳时携带 Cookie。 - 应用部署在多台服务器上,但未配置 Session 共享或粘性会话。
- 支付前没有实际创建 Session,或者代码调用过
session.invalidate()。 - 回调参数中的
userId没有正确传递或解析。Session 为null与userId为null是两个问题,需要分别排查。
将代码改成 request.getSession() 也无法取回原来的数据。它只会为当前回调请求创建一个新的空 Session。
推荐的处理流程
1. 支付前创建本地订单
跳转到支付网关前,先把订单写入数据库,并将初始状态设为 PENDING。订单至少需要保存:
- 商户订单号
- 用户 ID
- 应付金额和币种
- 当前支付状态
- 创建时间
- 支付网关交易号(收到回调后保存)
商户订单号需要设置唯一约束,并作为支付请求参数发送给网关。
String merchantOrderId = UUID.randomUUID().toString();
PaymentOrder order = new PaymentOrder();
order.setMerchantOrderId(merchantOrderId);
order.setUserId(userId);
order.setAmount(amount);
order.setStatus("PENDING");
paymentOrderDao.insert(order);
// 按 itz cash 实际接口规范组装支付参数
paymentRequest.setMerchantOrderId(merchantOrderId);
paymentRequest.setAmount(amount);
不要只在 Session 中保存 userId 或订单信息,否则网关的服务器通知无法读取这些数据。
2. 在回调中根据订单号查询订单
具体参数名以 itz cash 当前的接入文档为准。以下代码中的 merchantOrderId、transactionId 和 signature 仅用于说明处理方式:
@PostMapping("/payment/callback")
@ResponseBody
public String handlePaymentCallback(HttpServletRequest request) {
String merchantOrderId = request.getParameter("merchantOrderId");
String transactionId = request.getParameter("transactionId");
String status = request.getParameter("status");
String amount = request.getParameter("amount");
String signature = request.getParameter("signature");
if (merchantOrderId == null || signature == null) {
return "FAIL";
}
// 必须按照支付网关文档规定的字段顺序和算法验签
if (!itzCashService.verifySignature(request.getParameterMap(), signature)) {
return "FAIL";
}
PaymentOrder order = paymentOrderDao.findByMerchantOrderId(merchantOrderId);
if (order == null) {
return "FAIL";
}
if (!itzCashService.isSuccessfulStatus(status)) {
return "FAIL";
}
if (!order.getAmount().toPlainString().equals(amount)) {
return "FAIL";
}
paymentService.markPaid(
merchantOrderId,
transactionId,
new BigDecimal(amount)
);
return "SUCCESS";
}
比较支付金额时应使用 BigDecimal,并按照网关规定的金额单位和小数位进行规范化。不要使用 double 比较金额。
3. 让回调支持重复执行
如果请求超时,或者网关没有收到正确响应,支付网关可能会重复发送通知。数据库更新必须具备幂等性,防止重复插入订单、充值或发货。
@Transactional
public void markPaid(
String merchantOrderId,
String transactionId,
BigDecimal paidAmount) {
PaymentOrder order =
paymentOrderDao.findForUpdate(merchantOrderId);
if (order == null) {
throw new IllegalArgumentException("Order not found");
}
if ("PAID".equals(order.getStatus())) {
return;
}
if (order.getAmount().compareTo(paidAmount) != 0) {
throw new IllegalStateException("Payment amount mismatch");
}
int updated = paymentOrderDao.updateStatusIfPending(
merchantOrderId,
"PENDING",
"PAID",
transactionId
);
if (updated != 1) {
throw new IllegalStateException("Order status update failed");
}
}
商户订单号和支付网关交易号最好都设置唯一约束。
如何排查当前问题
先记录回调请求中必要的信息,但不要输出签名密钥、完整 Cookie、银行卡信息等敏感数据:
HttpSession session = request.getSession(false);
System.out.println("requestURL=" + request.getRequestURL());
System.out.println("method=" + request.getMethod());
System.out.println("session=" + session);
System.out.println("requestedSessionId=" + request.getRequestedSessionId());
System.out.println(
"requestedSessionIdValid=" + request.isRequestedSessionIdValid()
);
System.out.println("merchantOrderId="
+ request.getParameter("merchantOrderId"));
按以下顺序检查:
- 回调来自浏览器跳转,还是支付网关的服务器通知。
- 请求中是否带有
JSESSIONIDCookie。 - 回调地址与支付前页面是否使用相同的域名、协议和应用上下文。
- Session 是否已经过期。
- 多实例部署是否配置了共享 Session。
- 网关是否原样返回商户订单号。
- 回调的验签、状态判断和金额校验是否通过。
- 应用返回给网关的确认内容是否符合接口协议。
浏览器回跳页确实需要登录状态时
浏览器回跳页可以尝试读取 Session:
HttpSession session = request.getSession(false);
if (session != null) {
Object userId = session.getAttribute("userId");
if (userId != null) {
String ff = userId.toString();
Customer toCust =
daoLogin.getUserInfoForCustomer(userId);
}
}
如果采用跨站 POST 回跳,还要检查 Session Cookie 的 SameSite 配置。放宽 Cookie 策略会改变安全边界,因此必须同时配置 HTTPS、CSRF 防护和安全的 Cookie 选项。
即便浏览器回跳时能够取得 Session,也只能用来展示支付结果,不能将其作为确认付款的唯一依据。用户可能关闭页面,网络也可能中断,回跳参数还可能被伪造。最终支付状态应以通过签名验证的服务器通知,或主动查询支付网关得到的结果为准。
备注:内容仅供参考。
