
本文详解在 Spring Web 应用中,当外部不可控服务使用 ___ 代替 / 作为路径分隔符时,如何通过自定义 Filter 正确、安全地预处理请求路径,避免因 URL 编码双重转义或 Servlet 容器路径解析异常导致的 500 错误。
本文详解在 spring web 应用中,当外部不可控服务使用 `___` 代替 `/` 作为路径分隔符时,如何通过自定义 `filter` 正确、安全地预处理请求路径,避免因 url 编码双重转义或 servlet 容器路径解析异常导致的 500 错误。
在 Spring Web(尤其是基于 Servlet 的 MVC 或 WebFlux)中,直接修改 HttpServletRequest 的路径信息(如 getServletPath()、getRequestURI() 或 getPathInfo())并不能改变 Spring MVC 路由匹配的实际依据——因为 Spring 在 DispatcherServlet 初始化阶段已从原始 HttpServletRequest 中提取并缓存了路径变量、URI 等关键字段;单纯包装 HttpServletRequestWrapper 并重写 getServletPath() 是无效的,它不会影响 HandlerMapping 对 @PathVariable 的解析,更无法绕过 Tomcat/Jetty 等容器对 URL 的早期规范化校验(例如拒绝含非法字符或编码冲突的路径)。
你遇到的两个核心错误正印证了这一点:
- 第一次尝试中,originalUrl.replace("___", "/") 返回了完整 URL(如 "http://localhost:9091/infra/Test/Test"),但 getServletPath() 必须只返回上下文路径之后的相对路径部分(如 /infra/Test/Test),且不能包含协议和域名。返回完整 URL 导致 Spring 解析出错,报 “Invalid contextPath”;
- 第二次尝试中,replace("___", "%2F") 引发了双重 URL 编码:%2F 被容器再次编码为 %252F(% → %25),说明该替换发生在容器 URL 解析之后、Spring 处理之前,此时原始请求已被解码,手动插入编码值会触发二次编码。
✅ 正确解法:不篡改路径语义,而是在请求进入 Spring MVC 之前,将 ___ 归一化为 /,并确保所有路径相关方法(getRequestURI()、getContextPath()、getServletPath()、getPathInfo())返回一致、合法的值。
以下是一个经过验证的 OncePerRequestFilter 实现:
@Component
public class TripleUnderscoreToSlashFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (!request.getRequestURL().toString().contains("___")) {
filterChain.doFilter(request, response);
return;
}
String originalUri = request.getRequestURI();
String correctedUri = originalUri.replace("___", "/");
// 构建修正后的请求包装器
HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
@Override
public String getRequestURI() {
return correctedUri;
}
@Override
public String getServletPath() {
// ServletPath = URI - ContextPath(若存在)
String ctx = getContextPath();
return correctedUri.startsWith(ctx) ? correctedUri.substring(ctx.length()) : correctedUri;
}
@Override
public String getPathInfo() {
String servletPath = getServletPath();
String path = getRequestURI();
int idx = path.indexOf(servletPath);
return (idx >= 0 && servletPath.length() <p>⚠️ 关键注意事项:</p>
- 不要修改 getRequestURL() 或 getQueryString():它们应保持原始值,仅 getRequestURI() 及其衍生方法(getServletPath/getPathInfo)需同步修正;
- 务必校验 getContextPath() 长度与位置,避免 substring 报 StringIndexOutOfBoundsException;
- 若应用部署在非根路径(如 /myapp),getContextPath() 非空,getServletPath() 必须剔除上下文前缀后返回有效路径;
- 此方案兼容 @PathVariable、@RequestMapping 和 RESTful 路由,无需改动 Controller 层代码;
- 避免在 Filter 中做 URL 编码操作(如 %2F)——交由浏览器或客户端生成合法 URL,服务端只做语义等价替换。
? 替代建议(轻量级场景):
若业务允许且路径结构简单(如固定两段参数),可放弃 @PathVariable,改用 @RequestParam 接收 path 参数(如 /infra?path=Test___Test),再在 Controller 内部解析。这虽牺牲 REST 风格,但规避了底层路径处理复杂性,正如原答案所提:“改用请求参数更简单”。
总结:路径预处理必须精准作用于 Servlet 规范定义的 URI 相关方法,保持各方法间逻辑一致性,并尊重容器生命周期。盲目重写 getServletPath() 或注入编码字符,只会引发更隐蔽的解析失败。










