You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot动态上下文路径搭配PathVariable出现404问题

Spring Boot动态上下文路径@PathVariable匹配失效问题修复

问题背景

参考Spring Boot动态上下文路径实现方案做功能开发,固定路径接口访问符合预期,但Controller层使用@PathVariable路径变量时,动态前缀校验规则完全失效,非白名单前缀的请求也能正常命中接口。

问题复现代码

1. 配置类代码

@Configuration
public class Config {

    @Bean
    public WrapRequestFilter wrapRequestFilter() {
        return new WrapRequestFilter();
    }

    @Bean
    public FilterRegistrationBean wrapRequestFilterRegistrationBean() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(wrapRequestFilter());
        registrationBean.setName("wrapRequestFilter");
        registrationBean.setOrder(-1000001);
        return registrationBean;
    }
}

2. 自定义过滤器类

public class WrapRequestFilter extends OncePerRequestFilter {

    private static final String[] PATHS = new String[] { "/food", "/equipment" };

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        ServletContextFacadeRequestWrapper wrapper = new ServletContextFacadeRequestWrapper(request);
        String path = getMatchingContextPathForRequest(request);
        if (path != null) {
            wrapper.setContextPath(request.getContextPath() + path);
            String newPath = request.getServletPath().substring(path.length());
            if (newPath.length() == 0) {
                newPath = "/";
            }
            wrapper.setServletPath(newPath);
        }
        filterChain.doFilter(wrapper, response);
    }

    public String getMatchingContextPathForRequest(HttpServletRequest request) {
        for (String path : PATHS) {
            if (request.getServletPath().startsWith(path)) {
                return path;
            }
        }
        return null;
    }

}

3. 请求包装类

public class ServletContextFacadeRequestWrapper extends HttpServletRequestWrapper {

    private String contextPath;
    private String servletPath;

    public ServletContextFacadeRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String getContextPath() {
        if (StringUtils.isNotBlank(contextPath)) {
            return contextPath;
        }
        return super.getContextPath();
    }

    @Override
    public String getServletPath() {
        if (StringUtils.isNotBlank(servletPath)) {
            return servletPath;
        }
        return super.getServletPath();
    }

    @Override
    public String getRequestURI() {
        String requestURI = super.getRequestURI();
        if (requestURI.equals(contextPath)) {
            return requestURI + "/";
        }
        return requestURI;
    }

    public void setContextPath(String contextPath) {
        this.contextPath = contextPath;
    }

    public void setServletPath(String servletPath) {
        this.servletPath = servletPath;
    }
}

4. 测试Controller接口

  • 写法1(带@PathVariable,匹配失效):
@RestController
public class FruitsController {
    @PostMapping("/{source}/method2")
    public String fruitsMethod(@PathVariable("source") String source){
        return "method working";
    }
}
  • 写法2(无路径变量,匹配正常):
@RestController
public class FruitsController {
    @PostMapping("/method2")
    public String fruitsMethod(){
        return "method working";
    }
}

问题现象

  • 用无路径变量的写法2时,访问curl --location --request POST 'http://localhost:8080/food/method2'正常返回200,使用PATHS配置外的前缀访问返回404,符合预期。
  • 用带@PathVariable的写法1时,除/food、/equipment外的任意前缀路径都能正常返回200,动态上下文路径的前缀匹配规则完全失效。

根因分析

问题由两个逻辑缺陷共同导致:

  1. 请求路径相关方法重写不全:Spring MVC做路径匹配(尤其是带路径变量的Ant风格匹配)时,不会直接读取getServletPath()做匹配,而是会调用getRequestURI()获取完整请求路径,再减去getContextPath()得到最终用于匹配的路径。现有包装类只重写了getContextPath()和getServletPath(),getRequestURI()的实现没有同步剔除动态上下文前缀,也没有重写getPathInfo()方法,导致路径计算值不一致,匹配逻辑错乱。
  2. 非白名单请求直接放行:现有过滤器逻辑只对匹配到PATHS前缀的请求做路径重写,对不匹配前缀的请求直接放行到过滤器链。当后端存在根路径下带路径变量的通配接口时,非白名单前缀的请求会直接命中通配接口,绕过前缀校验。

修正方案

1. 补全请求包装类的方法重写

修正ServletContextFacadeRequestWrapper,补全getRequestURI()、getPathInfo()的实现,保证所有路径相关方法返回值逻辑一致:

public class ServletContextFacadeRequestWrapper extends HttpServletRequestWrapper {

    private String contextPath;
    private String servletPath;

    public ServletContextFacadeRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String getContextPath() {
        if (StringUtils.isNotBlank(contextPath)) {
            return contextPath;
        }
        return super.getContextPath();
    }

    @Override
    public String getServletPath() {
        if (StringUtils.isNotBlank(servletPath)) {
            return servletPath;
        }
        return super.getServletPath();
    }

    @Override
    public String getRequestURI() {
        // 未设置动态上下文时返回原始URI
        if (!StringUtils.isNotBlank(contextPath)) {
            return super.getRequestURI();
        }
        // 拼接contextPath和重写后的servletPath,保证路径一致性
        String uri = contextPath + servletPath;
        // 空路径补斜杠
        return uri.length() == 0 ? "/" : uri;
    }

    @Override
    public String getPathInfo() {
        // 路径已拆分到contextPath和servletPath,pathInfo返回null即可
        return null;
    }

    public void setContextPath(String contextPath) {
        this.contextPath = contextPath;
    }

    public void setServletPath(String servletPath) {
        this.servletPath = servletPath;
    }
}

2. 增加非白名单请求拦截逻辑

修改过滤器逻辑,对不匹配动态前缀、且不属于项目内置白名单(如静态资源、监控端点等,可根据自身业务调整)的请求,直接返回404,避免被通配接口命中:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    ServletContextFacadeRequestWrapper wrapper = new ServletContextFacadeRequestWrapper(request);
    String path = getMatchingContextPathForRequest(request);
    if (path != null) {
        wrapper.setContextPath(request.getContextPath() + path);
        String newPath = request.getServletPath().substring(path.length());
        if (newPath.length() == 0) {
            newPath = "/";
        }
        wrapper.setServletPath(newPath);
        filterChain.doFilter(wrapper, response);
    } else {
        // 可在此处添加不需要前缀校验的内置路径白名单判断,比如静态资源、actuator端点
        // 非白名单请求直接返回404
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
    }
}

修正后,带@PathVariable的接口只会在请求前缀匹配PATHS配置时正常响应,非白名单前缀请求会直接返回404,符合预期。


内容的提问来源于stack exchange,提问作者Siddharth Bhatia

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 15:12:45