PAYATHON 2026

Java 跳转 Payment Gateway 后 session 数据丢失

支付老李

结论

Payment Gateway 返回后,如果 session.getAttribute("id") 是 null,通常不是这个属性被单独删除了,而是返回请求没有携带原来的 JSESSIONID,服务器创建了新的 session。其他可能原因包括原 session 已过期、服务器重启,或者请求被负载均衡到没有共享 session 的节点。

支付流程不应依赖 HTTP session 保存关键状态。订单号、用户标识和支付状态都应持久化到数据库,再通过支付网关提供的商户订单号或回调参数重新关联。session 只适合作为辅助信息。

先确认是否还是同一个 session

分别记录保存和读取属性时的 session ID:

HttpSession session = request.getSession();
session.setAttribute("id", 120);

System.out.println("Before payment: " + session.getId());

返回接口中先不要创建新 session,而是检查原 session 是否存在:

HttpSession session = request.getSession(false);

if (session == null) {
    System.out.println("No existing session");
} else {
    System.out.println("After payment: " + session.getId());
    Integer id = (Integer) session.getAttribute("id");
    System.out.println("id = " + id);
}

如果两次 session.getId() 不同,或者 request.getSession(false) 返回 null,说明问题出在 session 的关联或生命周期上,与 setAttribute() 无关。

常见原因

浏览器通常使用 JSESSIONID Cookie 识别 session。跳转到第三方支付域名再返回时,以下情况可能导致浏览器不发送该 Cookie:

  • 返回地址的域名与创建 session 时不同,例如 www.example.com 和 pay.example.com
  • HTTP 与 HTTPS 混用
  • Cookie 的 Path 没有覆盖返回接口
  • SameSite 策略阻止跨站请求携带 Cookie
  • 浏览器禁用了 Cookie

尤其要注意支付网关通过表单 POST 跳回网站的情况。某些 SameSite=Lax 配置不会在跨站 POST 请求中发送 session Cookie。

如果业务确实需要在跨站返回请求中恢复 session,可以按照 Servlet 容器支持的配置方式,将 session Cookie 设置为:

SameSite=None; Secure

这种设置要求全程使用 HTTPS。Tomcat、Jetty、Spring Boot 等运行环境及不同版本的配置方式并不相同,不能直接套用同一段配置。

2. 返回 URL 进入了另一个应用或节点

应用部署在多个实例上时,原 session 可能只保存在创建它的节点中。返回请求被转发到其他节点后,看起来就像 session 丢失了。

可以采用以下方案:

  • 配置负载均衡会话粘滞
  • 使用 Redis 等共享 session 存储
  • 使用 Spring Session 等集中式 session 方案

即便使用了共享 session,支付业务的关键数据仍然需要写入数据库。

3. session 已失效

如果用户在支付页面停留太久,原 session 可能已经超时。服务器重启、应用重新部署或 session 存储异常,也可能使 session 消失。

可以调整超时时间,例如:

<session-config>
    <session-timeout>30</session-timeout>
</session-config>

这里的单位是分钟。延长超时时间只能减少问题发生的概率,不能保证支付流程可靠。

4. 返回地址与发起支付时使用的站点不一致

对于 Cookie 来说,下面这些地址可能属于不同的作用域:

https://example.com
https://www.example.com
https://pay.example.com
http://example.com

支付前后的协议、域名和端口应尽量保持一致。同时,可以在浏览器开发者工具中检查返回请求是否携带了 JSESSIONID。

推荐的实现方式

发起支付前创建订单,并将后续需要恢复的数据保存到数据库:

String orderNo = UUID.randomUUID().toString();

PaymentOrder order = new PaymentOrder();
order.setOrderNo(orderNo);
order.setUserId(120);
order.setStatus("PENDING");
paymentOrderService.save(order);

// 将 orderNo 作为支付网关支持的商户订单号传入
paymentGatewayClient.createPayment(orderNo);

收到支付网关的浏览器返回请求或服务器回调后,根据商户订单号查询订单:

String orderNo = request.getParameter("orderNo");

PaymentOrder order = paymentOrderService.findByOrderNo(orderNo);
if (order == null) {
    response.sendError(HttpServletResponse.SC_BAD_REQUEST);
    return;
}

// 必须验证支付网关签名,并向网关核验支付结果
if (!paymentGatewayService.verifyCallback(request)) {
    response.sendError(HttpServletResponse.SC_FORBIDDEN);
    return;
}

paymentOrderService.markPaid(orderNo);
Integer userId = order.getUserId();

具体参数名由支付网关定义,可能是 merchantOrderId、out_trade_no 或 reference。应使用网关提供的商户侧唯一标识字段,不能假定它一定叫 orderNo。

浏览器跳转与服务器回调要分开处理

支付网关通常提供两类通知:

  • 浏览器返回地址:用户完成支付后,浏览器跳回商户网站。这个页面可能被关闭、刷新或重复访问。
  • 服务器回调地址:支付网关直接请求商户服务器,通常不会携带用户浏览器的 JSESSIONID。

服务器回调本身无法读取用户的 session。最终支付状态应以经过签名验证的服务器通知或主动查询结果为准,不能根据浏览器返回页面或 session 是否存在来判断。

注意事项

  • 不要将用户 ID、金额或支付状态直接放入未签名的 URL 参数后直接信任。
  • 必须校验支付网关的签名、订单号、金额、币种和商户身份。
  • 回调接口需要支持幂等处理,防止重复通知造成重复入账。
  • 不建议将 JSESSIONID 放入支付回调 URL,否则可能带来 session 泄露或会话固定风险。
  • session.setAttribute("id", 120); 保存的是自动装箱后的 Integer,读取时可以转换为 Integer,但类型转换无法解决 session 已经变化的问题。
  • 即使 Cookie 或负载均衡配置已经修复,也应保留数据库订单关联机制,以处理 session 超时、浏览器关闭和异步回调等情况。

备注:内容仅供参考。