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,动态上下文路径的前缀匹配规则完全失效。
根因分析
问题由两个逻辑缺陷共同导致:
- 请求路径相关方法重写不全:Spring MVC做路径匹配(尤其是带路径变量的Ant风格匹配)时,不会直接读取
getServletPath()做匹配,而是会调用getRequestURI()获取完整请求路径,再减去getContextPath()得到最终用于匹配的路径。现有包装类只重写了getContextPath()和getServletPath(),getRequestURI()的实现没有同步剔除动态上下文前缀,也没有重写getPathInfo()方法,导致路径计算值不一致,匹配逻辑错乱。 - 非白名单请求直接放行:现有过滤器逻辑只对匹配到
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
相关产品推荐
相关产品推荐

