PAYATHON 2026

Spring MVC redirect 导致 URL 中的 JSESSIONID 被截断

支付小周

结论

不要让 Spring 或 Servlet 容器再次处理支付服务提供的 ;jsessionid=...。更稳妥的方式是直接返回 302,将完整的外部 URL 原样写入 Location 响应头。

还需要在浏览器的 Network 面板中确认,jsessionid 是在应用第一次返回重定向时丢失的,还是支付服务收到请求后再次重定向并将其移除的。

原因

这里的 jsessionid 不是普通的查询参数,而是 URL 路径参数:

/xyz/abc.jsp;jsessionid=...?... 

Servlet 容器也常用这种形式进行 URL 重写。如果重定向 URL 经过下面的方法处理:

response.encodeRedirectURL(url)

容器可能会把其中的 ;jsessionid=... 识别为本应用的会话标识,再根据当前的 Cookie 和 Session 状态将它删除或替换。

在某些配置下,Spring MVC 的 RedirectView 会调用 encodeRedirectURL()。因此,下面这种写法不一定适合携带第三方系统的 jsessionid:

return "redirect:" + targetUrl;

示例中还有一个细节:

;jsessionid= .worker11

等号后面有一个空格。空格不是合法的原始 URI 字符。如果空格只是排版问题,应将其删除;如果它确实存在于服务端返回值中,就必须按 URL 规则编码。支付平台正常提供的会话标识通常不应含有未编码的空格。

直接设置 Location 响应头

直接设置响应头可以绕过 RedirectView 和 URL Session 重写:

@GetMapping("/payment")
public void redirectToPayment(HttpServletResponse response) {
    String targetUrl =
            "https://xxz.net/xyz/abc.jsp;jsessionid=.worker11"
            + "?FRONTENDLANGUAGESELECTED=de";

    response.setStatus(HttpServletResponse.SC_FOUND);
    response.setHeader("Location", targetUrl);
}

不要再调用:

response.encodeRedirectURL(targetUrl)

也不要对整个 URL 使用 URLEncoder.encode(),否则 /、;、? 和 = 等结构字符也会被编码,导致 URL 结构错误。

也可以使用 ResponseEntity:

@GetMapping("/payment")
public ResponseEntity<Void> redirectToPayment() {
    String targetUrl =
            "https://xxz.net/xyz/abc.jsp;jsessionid=.worker11"
            + "?FRONTENDLANGUAGESELECTED=de";

    return ResponseEntity
            .status(HttpStatus.FOUND)
            .location(URI.create(targetUrl))
            .build();
}

URI.create() 会校验 URI,所以目标地址不能含有未编码的空格。

使用 RedirectView 时标记外部主机

如果仍要使用 Spring MVC 的 RedirectView,可以配置本应用自己的主机。未列出的主机会被视为外部主机,不再经过 response.encodeRedirectURL():

@GetMapping("/payment")
public RedirectView redirectToPayment() {
    String targetUrl =
            "https://xxz.net/xyz/abc.jsp;jsessionid=.worker11"
            + "?FRONTENDLANGUAGESELECTED=de";

    RedirectView view = new RedirectView(targetUrl);
    view.setHosts("www.my-application.example");
    view.setExposeModelAttributes(false);
    return view;
}

setHosts() 中应填写本应用允许进行 URL 编码的主机,而不是支付服务的主机。这样,xxz.net 就会被识别为外部地址。

如果当前 Spring 版本或项目配置不方便使用这个选项,直接设置 Location 响应头会更明确。

确认究竟是哪一次重定向删除了参数

浏览器地址栏只会显示最终地址,无法据此判断第一次响应中的 Location 是否已经丢失 jsessionid。需要在开发者工具的 Network 面板中查看完整的跳转过程:

  1. 找到本应用返回的 302 响应。
  2. 查看该响应的 Location 响应头。
  3. 检查支付服务是否又返回了 301、302、303 或 307。

也可以使用:

curl -v --max-redirs 0 https://your-application.example/payment

如果第一次响应已经变成:

Location: https://xxz.net/xyz/abc.jsp?FRONTENDLANGUAGESELECTED=de

说明参数是在本应用、Servlet 容器或响应包装器中被移除的。

如果第一次响应包含完整地址:

Location: https://xxz.net/xyz/abc.jsp;jsessionid=.worker11?FRONTENDLANGUAGESELECTED=de

但支付服务随后返回的地址不再包含 jsessionid,问题就在支付服务端,Spring MVC 无法阻止这次后续重定向。

注意事项

支付服务生成的 URL 应尽量原样使用。不要将其拆开后重新拼装,也不能用当前应用自己的 HttpSession#getId() 替换第三方会话标识。

如果确实需要动态拼接,只编码参数值,不要编码整个 URL:

String encodedSessionId =
        UriUtils.encodePathSegment(sessionId, StandardCharsets.UTF_8);

String targetUrl =
        "https://xxz.net/xyz/abc.jsp;jsessionid="
        + encodedSessionId
        + "?FRONTENDLANGUAGESELECTED=de";

另外需要注意:

  • 示例中的域名一处是 xxz.et,另一处是 xxz.net,需要确认实际地址是否存在拼写错误。
  • 外部重定向目标应使用域名白名单,避免产生开放重定向漏洞。
  • URL 中的 Session ID 可能会出现在浏览器历史、代理日志和访问日志中,因此应避免记录完整地址。
  • 如果支付服务支持一次性 Token、表单 POST 或官方 SDK,应优先采用其推荐的跳转方式。

备注:内容仅供参考。