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 面板中查看完整的跳转过程:
- 找到本应用返回的
302响应。 - 查看该响应的
Location响应头。 - 检查支付服务是否又返回了
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,应优先采用其推荐的跳转方式。
备注:内容仅供参考。
