Swagger3 文档报错:拦截器误拦 /v3/api-docs
1. 问题现场还原与根因定位1.1 这个报错到底在抱怨什么Unable to infer base url和Unable to render this definition这两个提示几乎可以算是 Spring Boot 项目接入 Swagger3也就是 springdoc-openapi之后最常见的“见面礼”。前者通常出现在 Swagger UI 页面的顶部横幅里页面能打开但下拉框里的分组是空的或者干脆连接口列表都渲染不出来后者则更彻底一点页面直接白屏中间一行红字告诉你 definition 渲染失败。很多人第一次遇到的时候会本能地怀疑依赖版本不对反复升级降级springdoc-openapi-ui折腾半天发现问题照旧——因为真正的凶手大概率不在 Swagger 本身而在你自己写的那个HandlerInterceptor上。先把结论摆在前面springdoc-openapi 在启动和运行时会通过一组固定的 HTTP 端点向服务端索取文档元数据这些端点的路径是硬编码在SwaggerWelcomeWebMvc、OpenApiWebMvcResource这些类里的。只要你的拦截器对/v3/api-docs、/v3/api-docs/swagger-config、/swagger-ui/**这些路径做了登录校验、Token 校验、或者统一返回了自定义的错误 JSONSwagger UI 拿不到它期待的响应体就会在浏览器端抛出这两个错误。换句话说这不是 Swagger 坏了是你把它的“口粮”给截了。我第一次踩这个坑是在一个前后端分离的项目里后端加了统一的 JWT 拦截器对所有非白名单路径做鉴权。Swagger UI 的静态资源能加载但/v3/api-docs/swagger-config返回的是{code:401,msg:未登录}于是页面顶部就顶着那行 base url 推断失败的提示。当时查了两小时最后发现只需要在拦截器的addPathPatterns里排除掉那几个路径就完事了。这个经历让我意识到这两个错误的本质是“拦截器与文档端点抢路由”而不是 Swagger 的配置问题。1.2 三个关键端点的职责划分要理解为什么会被拦截得先搞清楚 springdoc-openapi 到底请求了哪些地址。很多人只知道/v3/api-docs其实完整的链路包含三个角色。端点路径作用谁在请求被拦截后的典型症状/v3/api-docs/swagger-config返回 UI 的配置信息包含分组列表、URL 前缀、认证参数等Swagger UI 前端 JSUnable to infer base url分组下拉框为空/v3/api-docs返回默认分组的 OpenAPI JSON 文档Swagger UI 前端 JSUnable to render this definition/v3/api-docs/{group}返回指定分组的文档分组名由GroupedOpenApi配置Swagger UI 前端 JS切换分组时报错或空白/swagger-ui/**UI 静态资源含 JS、CSS、HTML浏览器页面 404 或样式错乱这张表建议直接存进你的排查笔记。实际排查时打开浏览器开发者工具的 Network 面板刷新 Swagger 页面看这几个请求的响应状态码和响应体。如果/v3/api-docs/swagger-config返回 200 但内容是{code:401}之类的自定义结构那基本可以锁定是拦截器把它当业务接口处理了。如果返回的是 302 重定向到登录页那说明拦截器做了重定向而非直接返回 JSON这种情况下 UI 会尝试跟随重定向最终拿到的是一段 HTML解析 JSON 时失败报的也是 base url 推断错误。1.3 为什么拦截器会“误伤”文档端点默认情况下Spring MVC 的拦截器是全局生效的只要你在WebMvcConfigurer的addInterceptors里注册了它并且addPathPatterns(/**)那它就会拦截一切进入 DispatcherServlet 的请求包括 springdoc 注册的那些 handler。这里有一个容易被忽略的细节springdoc 的端点是通过RestController或者RequestMappingHandlerMapping动态注册的它们和你的业务 Controller 走的是同一套请求映射流程所以拦截器天然会命中它们。有些人会想“那我用Bean注册的HandlerInterceptor不就是为了统一鉴权吗”问题在于统一鉴权的前提是“所有需要鉴权的接口”而文档端点在大多数开发环境里恰恰是不需要鉴权的。把不需要鉴权和需要鉴权的接口混在同一套规则里就必然要显式排除。更麻烦的是有些团队在拦截器里直接读取HttpServletRequest的 header 判断 tokentoken 缺失时直接response.getWriter().write(...)并把响应标记为已完成这种行为会彻底切断 springdoc 端点的正常返回链路。还有一个隐蔽的坑如果你在拦截器里调用了response.sendRedirect()或者返回了false但没写响应体浏览器会收到一个空响应或重定向Swagger UI 的 JS 代码在解析时会抛出异常。所以排查这个问题光看后端日志有时不够必须结合浏览器 Network 面板一起看。2. 核心方案选型与配置思路2.1 主流解决路径的横向对比遇到这个问题网上的方案五花八门但归纳起来其实就是四类思路。我在不同项目里都试过每种都有适用场景不能一概而论。第一种是拦截器路径排除也就是在注册拦截器时用excludePathPatterns把文档相关路径排除掉。这是最直接、侵入性最小的方案适合绝大多数中小项目。第二种是在拦截器内部判断路径通过request.getRequestURI()判断当前请求是否属于文档端点是则直接放行。第三种是调整文档端点的前缀把 springdoc 的默认路径改成一个业务拦截器不覆盖的路径比如写成/api-docs之外的/doc-internal。第四种是给文档端点单独开一个端口通过management.server.port或自定义 Servlet 容器实现适合对安全隔离要求高的场景。方案改动成本安全性适用场景潜在副作用excludePathPatterns 排除低中开发/测试环境内网部署生产环境若未关闭文档则暴露接口拦截器内判断 URI 放行中中需要在拦截器里做细粒度控制的场景判断逻辑分散维护成本略高自定义文档前缀低中高希望隐藏默认路径的项目前端或其他系统引用旧路径需同步改独立端口承载文档高高生产环境需严格隔离配置复杂需要额外的端口管理我的建议是开发和测试环境用第一种简单粗暴且不容易出错生产环境要么关闭 Swagger要么用第二种加开关控制不要图省事在生产也放开所有文档端点。这里顺便提一句很多团队会在生产环境用springdoc.api-docs.enabledfalse和springdoc.swagger-ui.enabledfalse直接关掉文档这样拦截器爱怎么拦都无所谓因为端点根本不存在。2.2 为什么排除路径比改路径更优先有人可能会问既然改前缀也能解决为什么不直接改前缀一劳永逸这里面有个现实考量改前缀虽然能让当前项目的拦截器不再命中但一旦项目里有其他组件硬编码了/v3/api-docs比如前端团队自己写的接口调试页面、API 网关的文档聚合配置、或者 CI 流程里的文档校验脚本就会连锁失效。我在一个项目里就遇到过网关层聚合了多个微服务的 Swagger 文档结果其中一个服务改了前缀网关那边直接拉不到文档排查了好久才定位到。而excludePathPatterns是纯后端改动影响面可控且语义清晰——它明确表达了“这些路径不走我的业务鉴权”。从可维护性角度看这种显式排除比隐式改路径更容易被后来接手的人理解。所以除非你有明确的安全诉求需要隐藏默认路径否则优先用排除。2.3 拦截器注册的正确姿势与常见误区在 Spring Boot 里注册拦截器的标准写法是实现WebMvcConfigurer接口重写addInterceptors方法然后往InterceptorRegistry里添加。这里有几个细节值得展开说。Configuration public class WebMvcConfig implements WebMvcConfigurer { Autowired private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/**) .excludePathPatterns( /v3/api-docs/**, /swagger-ui/**, /swagger-ui.html, /swagger-resources/**, /webjars/**, /doc.html, /favicon.ico ); } }这段代码看起来很普通但有几个坑必须点出来。第一/v3/api-docs/**和/v3/api-docs的区别。Spring 的路径匹配里/**能匹配多级路径但/v3/api-docs这个端点本身是单级路径/v3/api-docs/swagger-config是两级。如果你只写/v3/api-docs/**在较老的 Spring 版本里可能匹配不到/v3/api-docs本身。稳妥的做法是两个都写或者直接用/v3/api-docs/**配合/v3/api-docs一起排除。我在 Spring Boot 2.3 的项目里实测过只写/v3/api-docs/**时某些版本确实会漏掉裸路径。第二/swagger-ui/**和/swagger-ui.html要分开写。swagger-ui.html是 Springfox 时代的入口springdoc-openapi 默认的 UI 入口是/swagger-ui/index.html但为了兼容很多人还是会把swagger-ui.html加上。第三/webjars/**千万别漏Swagger UI 的 JS 和 CSS 是通过 webjars 依赖提供的漏掉这个路径会导致页面样式全无虽然不会报那两个错但体验极差。注意如果你用的是 Spring Security 而不是自定义拦截器情况会略有不同。Spring Security 的过滤器链在 DispatcherServlet 之前执行需要在WebSecurityConfigurerAdapter或 SecurityFilterChain 里对文档路径放行否则请求根本到不了拦截器这一层。两套机制的排除写法经常被人混淆。3. 实操配置全流程与代码拆解3.1 从零搭建一个可复现的报错环境为了把这套排查逻辑讲透我先带你复现一次问题。环境是 Spring Boot 2.7.x 加 springdoc-openapi 1.6.xJDK 8 或 11 都行。先在pom.xml里引入依赖注意 springdoc-openapi 的版本要和 Spring Boot 大版本匹配。Spring Boot 2.x 用 1.x 系列Spring Boot 3.x 用 2.x 系列这个对应关系弄错了会出现一堆莫名其妙的问题。dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-ui/artifactId version1.6.15/version /dependency然后写一个最简单的鉴权拦截器故意不做任何路径排除制造问题现场Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录\}); return false; } return true; } }注册时不排除任何路径Configuration public class WebMvcConfig implements WebMvcConfigurer { Autowired private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor).addPathPatterns(/**); } }启动项目访问http://localhost:8080/swagger-ui/index.html你就会看到页面顶部出现Unable to infer base url的提示或者接口列表完全是空的。此时打开 Network 面板能看到/v3/api-docs/swagger-config返回了 401 和那段自定义 JSON。问题复现成功。3.2 按端点逐个放行的最小改动方案复现之后修复其实很简单就是在excludePathPatterns里补上文档路径。但我不建议只补一个/v3/api-docs/**就完事而是按端点语义分组排除这样后续维护时一眼能看懂。Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/**) .excludePathPatterns( // springdoc 文档元数据端点 /v3/api-docs, /v3/api-docs/**, // Swagger UI 静态资源 /swagger-ui.html, /swagger-ui/**, // webjars 依赖的 JS/CSS /webjars/**, // 兼容旧版与第三方 UI /swagger-resources/**, /doc.html, // 浏览器默认请求 /favicon.ico ); }改完重启再次访问 UI那两个错误应该都消失了。这里我想强调一点只排除/v3/api-docs/**有时候还不够因为swagger-config这个端点在某些版本的实现里走的是另一个 handler。我遇到过只排除/v3/api-docs/**后/v3/api-docs/swagger-config依然被拦的情况所以显式写出裸路径是更稳妥的做法。3.3 如果不想改拦截器在拦截器内部做 URI 判断有些团队因为架构原因不方便修改拦截器的注册配置比如拦截器是通过 starter 自动装配进来的改不了别人写的配置类。这种情况下可以在拦截器内部做判断属于文档端点的请求直接放行。Component public class AuthInterceptor implements HandlerInterceptor { private static final ListString WHITE_LIST Arrays.asList( /v3/api-docs, /swagger-ui, /swagger-resources, /webjars, /doc.html ); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String uri request.getRequestURI(); for (String prefix : WHITE_LIST) { if (uri.startsWith(prefix)) { return true; } } // 下面是正常的鉴权逻辑 String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录\}); return false; } return true; } }这种写法的好处是放行逻辑集中在一处不依赖外部的路径排除配置坏处是白名单和业务代码耦合未来如果 springdoc 换了默认路径还得改代码。我的经验是如果项目里拦截器不多优先用配置排除如果拦截器是通过框架统一管理的那就只能走代码判断这条路。另外要注意request.getRequestURI()返回的路径可能带 context-path如果项目配置了server.servlet.context-path判断时需要把它考虑进去否则前缀匹配会失败。3.4 生产环境的安全收口与开关设计开发环境放行文档没问题但生产环境直接放行所有文档端点等于把你的接口结构、参数定义全暴露了。我的做法是用配置开关把文档的启用与鉴权分离通过 profile 控制。# application-dev.yml springdoc: api-docs: enabled: true swagger-ui: enabled: true # application-prod.yml springdoc: api-docs: enabled: false swagger-ui: enabled: false配合拦截器里的条件放行可以用Value注入环境标记生产环境即使有人误加了白名单文档端点也是关闭的双重保险。Value(${springdoc.api-docs.enabled:false}) private boolean apiDocsEnabled;然后在拦截器里判断如果文档端点未启用就走正常鉴权流程不给任何放行机会。这套组合我在几个对外服务上都用过既保证了开发效率又堵住了生产泄露的口子。还有一个细节如果你用了网关聚合文档生产环境的聚合端点也需要单独处理不能简单地把所有服务的文档都放开。注意关闭文档端点后如果拦截器白名单还残留着/v3/api-docs/**虽然端点不存在了不会造成泄露但会让后来接手的人产生困惑。建议关闭时同步清理白名单保持配置的一致性。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →