PAYATHON 2026

多个服务器跳转时如何保持同一用户 session?

支付小周

结论

IRCTC 和银行不会共享用户 Session。支付后登录状态依然存在,是因为 IRCTC 的 Session 始终保存在自己的服务器上,浏览器也保留着 IRCTC 域名下的 Session Cookie。

跳转到银行网站时,浏览器不会向银行发送 IRCTC 的 Session Cookie。支付完成并返回 IRCTC 后,浏览器会再次携带这个 Cookie,IRCTC 据此识别原来的用户。

支付过程中传递的通常是订单号、支付金额、交易流水号和签名,不是 HttpSession 对象或 JSESSIONID。

离开网站后,Session 为什么没有结束

用户登录 IRCTC 后,服务器通常会创建一个 HttpSession,并通过响应设置类似下面的 Cookie:

Set-Cookie: JSESSIONID=ABC123; Path=/; Secure; HttpOnly; SameSite=Lax

浏览器会保存这个 Cookie。只要 Session 尚未过期,也没有被主动注销,浏览器返回 IRCTC 时仍会发送:

Cookie: JSESSIONID=ABC123

服务器根据 JSESSIONID 找到原来的 HttpSession,用户也就仍处于登录状态。

Cookie 受域名限制:

irctc.example 的 Cookie -> 只发给 irctc.example
bank.example 的 Cookie  -> 只发给 bank.example

所以,银行正常情况下无法读取 IRCTC 的 Session Cookie,IRCTC 也不会读取银行的 Session Cookie。双方维护的是两个独立的 Session。

典型的支付跳转流程

整个过程大致如下:

用户浏览器
   |
   | 1. 登录并创建订单
   v
IRCTC
   |
   | 2. 返回银行支付参数和支付地址
   v
用户浏览器
   |
   | 3. POST 或重定向到银行
   v
CITI Bank
   |
   | 4. 用户完成支付
   |
   | 5. 银行通知 IRCTC 支付结果
   v
IRCTC 支付回调接口
   ^
   |
   | 6. 浏览器跳转回支付结果页面
用户浏览器

支付结果一般通过两条路径返回:

  • 浏览器返回:把用户带回 IRCTC 的支付结果页面。
  • 服务端回调:银行服务器直接把支付结果通知给 IRCTC。

实际业务不能只相信浏览器带回的参数。用户可能关闭页面,网络也可能中断,请求本身还可能被伪造。订单的最终状态应以通过银行签名验证的服务端通知或主动查询结果为准。

IRCTC 如何向银行发送请求

跨域跳转不能使用 RequestDispatcher。RequestDispatcher.forward() 只能在当前 Web 应用内部转发请求,无法将请求转发到其他网站。

如果银行接受 HTTP GET,可以使用:

response.sendRedirect(bankUrl);

支付参数通常包含较多字段,因此更常见的做法是生成一个自动提交的 HTML 表单,通过 POST 把数据提交给银行:

<form id="paymentForm"
      method="post"
      action="https://payment.citibank.example/pay">
    <input type="hidden" name="merchantId" value="${merchantId}">
    <input type="hidden" name="orderId" value="${orderId}">
    <input type="hidden" name="amount" value="${amount}">
    <input type="hidden" name="currency" value="INR">
    <input type="hidden" name="returnUrl" value="${returnUrl}">
    <input type="hidden" name="notifyUrl" value="${notifyUrl}">
    <input type="hidden" name="signature" value="${signature}">
</form>

<script>
    document.getElementById("paymentForm").submit();
</script>

银行收到的只是这些支付参数。字段如何命名、采用哪种签名算法、是否需要加密,都应以相应支付接口的协议文档为准。

不要向银行提交以下内容:

  • JSESSIONID
  • 用户密码
  • Session 中的对象
  • 能够直接修改订单状态的内部参数
  • 未签名的金额或订单信息

使用 Struts 发起支付

在 Struts 中,通常由一个 Action 处理”去支付”请求。这个 Action 需要完成以下工作:

  1. 检查用户是否已经登录。
  2. 从数据库读取订单,不能直接相信页面提交的金额。
  3. 创建唯一的支付流水。
  4. 保存订单和当前用户之间的关系。
  5. 按照银行协议生成签名。
  6. 跳转到包含自动提交表单的 JSP。

下面以传统 Struts 1 风格展示基本结构。具体类名和配置需要根据项目版本调整:

public class StartPaymentAction extends Action {

    @Override
    public ActionForward execute(
            ActionMapping mapping,
            ActionForm form,
            HttpServletRequest request,
            HttpServletResponse response) throws Exception {

        HttpSession session = request.getSession(false);

        if (session == null || session.getAttribute("loginUserId") == null) {
            return mapping.findForward("login");
        }

        String userId = (String) session.getAttribute("loginUserId");
        String orderId = request.getParameter("orderId");

        // 必须从数据库读取并校验订单归属、状态和金额
        Order order = orderService.findPayableOrder(orderId, userId);

        PaymentRequest payment = paymentService.createPaymentRequest(order);

        request.setAttribute("merchantId", payment.getMerchantId());
        request.setAttribute("orderId", payment.getOrderId());
        request.setAttribute("amount", payment.getAmount());
        request.setAttribute("returnUrl", payment.getReturnUrl());
        request.setAttribute("notifyUrl", payment.getNotifyUrl());
        request.setAttribute("signature", payment.getSignature());

        return mapping.findForward("bankPaymentForm");
    }
}

对应的 Struts 配置可以写成:

<action
    path="/startPayment"
    type="com.example.payment.StartPaymentAction"
    scope="request">
    <forward
        name="login"
        path="/login.jsp"/>
    <forward
        name="bankPaymentForm"
        path="/WEB-INF/jsp/bank-payment-form.jsp"/>
</action>

Action 并不负责和银行”共享 Session”。它只负责准备支付参数,再让浏览器向银行发起一个新请求。

如果支付平台提供服务端下单 API,Action 可以先通过 HTTPS 调用银行接口,获取支付地址或支付令牌,然后让浏览器跳转过去。

银行如何把用户带回 IRCTC

发起支付时,IRCTC 通常会向银行提供返回地址:

https://irctc.example/payment/return.do

支付结束后,银行可能返回类似参数:

orderId=ORD-20260901-001
transactionId=CITI-998877
status=SUCCESS
signature=...

银行可以使用两种返回方式。第一种是:

HTTP 302 Redirect

第二种是提交 HTML 表单:

<form method="post" action="https://irctc.example/payment/return.do">
    <input type="hidden" name="orderId" value="ORD-20260901-001">
    <input type="hidden" name="transactionId" value="CITI-998877">
    <input type="hidden" name="status" value="SUCCESS">
    <input type="hidden" name="signature" value="...">
</form>

这并不是”同一个请求返回”。从 HTTP 的角度看,每次跳转或表单提交都会生成一个新请求:

请求 1:浏览器 -> IRCTC
请求 2:浏览器 -> CITI Bank
请求 3:浏览器 -> IRCTC

服务器之间也没有发生所谓的”切换”。浏览器根据响应中的跳转地址或表单地址,决定下一个请求要发给哪个服务器。

Struts 如何接收银行返回的结果

可以为银行返回页面配置一个独立的 Action:

public class PaymentReturnAction extends Action {

    @Override
    public ActionForward execute(
            ActionMapping mapping,
            ActionForm form,
            HttpServletRequest request,
            HttpServletResponse response) throws Exception {

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

        PaymentResult result = new PaymentResult(
                orderId,
                transactionId,
                status,
                signature
        );

        if (!paymentService.verifySignature(result)) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return null;
        }

        /*
         * 不应仅凭浏览器返回的 status 将订单标记为支付成功。
         * 应结合服务端回调,或调用银行查询接口核实交易。
         */
        PaymentStatus verifiedStatus =
                paymentService.queryVerifiedStatus(orderId, transactionId);

        request.setAttribute("paymentStatus", verifiedStatus);
        return mapping.findForward("result");
    }
}

配置示例:

<action
    path="/payment/return"
    type="com.example.payment.PaymentReturnAction"
    scope="request">
    <forward
        name="result"
        path="/WEB-INF/jsp/payment-result.jsp"/>
</action>

服务端通知应使用另一个接口,例如:

POST https://irctc.example/payment/notify.do

这个接口通常不依赖用户 Session,因为请求来自银行服务器,不是用户浏览器。接口应根据商户订单号查找订单,验证签名,并以幂等方式更新支付状态。

public class PaymentNotifyAction extends Action {

    @Override
    public ActionForward execute(
            ActionMapping mapping,
            ActionForm form,
            HttpServletRequest request,
            HttpServletResponse response) throws Exception {

        PaymentNotification notification =
                paymentService.parseNotification(request);

        if (!paymentService.verifySignature(notification)) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return null;
        }

        // 重复通知不能导致重复扣款、重复出票或重复入账
        paymentService.processNotificationIdempotently(notification);

        response.setContentType("text/plain;charset=UTF-8");
        response.getWriter().write("SUCCESS");
        return null;
    }
}

返回后为什么可能拿不到原来的 Session

现代浏览器的 SameSite Cookie 策略会影响跨站返回时是否发送 Session Cookie。

常见情况包括:

  • 银行通过顶层 GET 跳转返回时,SameSite=Lax 通常允许浏览器携带 IRCTC Cookie。
  • 银行通过跨站 POST 表单返回时,SameSite=Lax 或 Strict 可能导致浏览器不发送 Cookie。
  • 如果跨站 POST 确实需要携带 Cookie,通常要设置 SameSite=None; Secure,而且站点必须使用 HTTPS。

例如:

Set-Cookie: JSESSIONID=ABC123; Path=/; Secure; HttpOnly; SameSite=None

具体配置取决于 Servlet 容器及其版本,应查阅实际使用的 Tomcat、WebLogic 或其他容器文档。

即使返回请求没有携带原来的 Session,系统也不能因此失去确认支付结果的能力。可以通过不可预测的支付流水号、订单号、签名和服务端查询恢复支付结果。之后再要求用户重新登录,并展示属于该用户的订单。

IRCTC 部署多台应用服务器时怎么处理

如果”多个服务器”指的是 IRCTC 自己部署了多台应用服务器,还需要处理集群内部的 Session 管理。常见方案包括:

  • 让负载均衡器使用 sticky session,使同一用户持续访问同一节点。
  • 将 Session 保存在 Redis 等共享存储中。
  • 使用容器提供的 Session replication。
  • 尽量减少 Session 状态,改用可验证的身份令牌和数据库状态。

这是 IRCTC 集群内部的问题,与 IRCTC 和银行之间的跨域跳转无关。IRCTC 的多个节点可以共享自己的登录状态,但不应与银行共享 Session。

RequestDispatcher 和 sendRedirect() 有什么区别

正确的 API 用法分别是:

request.getRequestDispatcher("/internal.jsp").forward(request, response);

以及:

response.sendRedirect("https://payment.example/pay");

RequestDispatcher 没有 sendRedirect() 方法。

两者的区别如下:

  • forward() 在服务器内部完成,只适用于当前 Web 应用能够访问的资源,浏览器地址通常不会变化。
  • sendRedirect() 向浏览器返回重定向响应,浏览器随后向新 URL 发起请求,因此可以跳转到外部网站。
  • 如果需要向外部网站提交 POST 参数,通常要使用自动提交的 HTML 表单,而不是 sendRedirect()。
  • 如果服务器之间需要直接交换数据,应调用银行提供的 HTTPS API,而不是使用 RequestDispatcher。

实现时需要特别注意什么

  • 全程使用 HTTPS。
  • 不要向银行传递 JSESSIONID 或任何登录凭证。
  • 金额、订单归属和支付状态必须通过服务端数据源校验。
  • 银行返回的所有参数都应视为不可信输入。
  • 验证银行规定的数字签名、证书或消息认证码。
  • 校验订单号、商户号、金额、币种和交易流水号。
  • 避免重复通知引发重复出票或重复入账。
  • 不能让支付成功页面直接决定订单已经支付。
  • 同时支持浏览器返回、服务端通知和主动查询。
  • 为支付流水设置唯一约束,并记录必要的审计信息。
  • 不要只依赖 Referer 判断请求是否来自银行。
  • 如果返回地址接受用户输入,必须防止开放重定向。
  • 不要把密码、银行卡数据或其他敏感信息放进 URL 查询参数。

登录状态由 IRCTC 自己维护,支付状态则通过订单和支付流水建立关联。银行按照协议接收支付请求,并返回经过验证的交易结果。双方不需要,也不应该维护”同一个 Session”。

备注:内容仅供参考。