PAYATHON 2026

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"));

按以下顺序检查:

  1. 回调来自浏览器跳转,还是支付网关的服务器通知。
  2. 请求中是否带有 JSESSIONID Cookie。
  3. 回调地址与支付前页面是否使用相同的域名、协议和应用上下文。
  4. Session 是否已经过期。
  5. 多实例部署是否配置了共享 Session。
  6. 网关是否原样返回商户订单号。
  7. 回调的验签、状态判断和金额校验是否通过。
  8. 应用返回给网关的确认内容是否符合接口协议。

浏览器回跳页确实需要登录状态时

浏览器回跳页可以尝试读取 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,也只能用来展示支付结果,不能将其作为确认付款的唯一依据。用户可能关闭页面,网络也可能中断,回跳参数还可能被伪造。最终支付状态应以通过签名验证的服务器通知,或主动查询支付网关得到的结果为准。

备注:内容仅供参考。