接手过一个项目,一打开浏览器访问http://localhost:8080/swagger-ui.html,整个后端的接口文档直接裸奔在公网环境里。当时那份项目里连个最简单的登录校验都没有,Swagger 页面就这么大摇大摆地暴露了所有 Controller 的入参、出参和内部接口路径。说实话,这种状态放在内网开发环境还能忍,一旦部署到测试服务器或者稍微正式一点的环境,就相当于把自家大门的钥匙挂在门锁上。所以这个需求很直接:给访问swagger-ui.html加上一个登录页面,只有通过认证的人才能看到接口文档。
这篇东西就围绕 SpringBoot 项目里如何给 Swagger 文档页加登录保护来写。我会把从方案选型、拦截器写法、登录页制作、静态资源放行,到版本适配和排坑的完整过程都过一遍。适合正在维护 SpringBoot 项目的后端开发,尤其是那些项目里还没引入 Spring Security、需要快速给接口文档加一把锁的团队。
1. 为什么非要给 Swagger 页面加登录保护
1.1 默认配置下 Swagger 到底有多“裸”
很多 SpringBoot 新手把springfox-swagger2和springfox-swagger-ui依赖一加,配置类写好Docket,启动项目后swagger-ui.html一打开,心里还挺美:接口文档自动生成了。但实际上,如果项目里没有任何安全框架,没有任何拦截器,那么这个swagger-ui.html就是完全公开的。
SpringBoot 中像swagger-ui.html这种资源路径,默认不需要经过任何认证逻辑,因为 Spring MVC 的DispatcherServlet会把它当作普通的静态资源或 Controller 映射请求来处理。只要项目跑起来,任何人都可以直接访问到全部接口信息。内网里还好说,一旦部署到公网服务器或者通过反向代理暴露出去,等于把项目所有 API 的地址、请求方式、字段名、类型约束全部告诉了别人。我之前见过一个团队把生产环境的 Swagger 开着整整几个月,结果被外部扫描工具扫描到,整个接口结构被扒得干干净净。
1.2 三种加锁思路的对比
给 Swagger 加登录保护,常见的有三条路:
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| Spring Security | 集成安全框架,配置 HttpSecurity 对指定路径做 formLogin 或 httpBasic 认证 | 功能完整,支持角色、权限、记住我等 | 引入依赖较重,配置复杂度高 |
| HandlerInterceptor | 写一个拦截器,拦截/swagger-ui.html、/swagger-resources/**、/v3/api-docs/**,未登录则重定向到登录页 | 轻量、灵活、不引入额外依赖,适合中小项目 | 需要自己实现会话管理、密码校验 |
| Filter | 通过OncePerRequestFilter对 Swagger 请求做过滤 | 比拦截器更底层,可以处理所有 MVC 之外的路径 | 处理路径逻辑偏底层,不方便拿到 Handler 信息 |
对于一个没有引入 Spring Security 的 SpringBoot 项目,我自己更倾向于选择 HandlerInterceptor。原因很简单:SpringBoot 项目的拦截器机制已经很成熟,我们只是需要一个“未登录就跳登录页”的行为,根本不需要为这个功能引入一整套安全框架。而且拦截器能拿到HttpServletRequest和HttpServletResponse,重定向、写 JSON、处理 Session 都很顺手。
1.3 最终选型思路
我最终选用的方案是:SpringBoot 自带的拦截器 + Session 会话标记 + 一个手写的静态登录页。整体架构是这样:
- 用户访问
/swagger-ui.html。 - 拦截器判断 Session 里是否存在登录标记(比如
swagger_login)。 - 没有标记,直接重定向到
/swagger-login.html。 - 登录页提交用户名密码到
/swagger-login接口。 - 登录成功,Session 写入标记,再重定向回
/swagger-ui.html。
这样做的好处是:第一,没有额外依赖;第二,逻辑透明,后面的人接手代码一看就懂;第三,登录状态和业务系统的 Session 天然隔离,不干扰已有的用户体系。那接下来就按这个思路直接把实现写出来。
2. 核心实现:登录页、拦截器与配置
2.1 项目基础环境
示例代码以 SpringBoot 2.x 为主,因为标题里提到的swagger-ui.html是 springfox 时代的经典路径。核心依赖大概是:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>如果你的项目已经升级到 Springdoc / OpenAPI 3,也就是访问地址变成swagger-ui/index.html的情况,这套拦截器的思路完全通用,只需要把拦截路径改一下。后面我会单独讲适配差异,这里先按经典的 springfox 配置继续。
2.2 自定义登录拦截器
先写一个最核心的SwaggerAuthInterceptor。这个类继承HandlerInterceptorAdapter或者直接实现HandlerInterceptor都行。SpringBoot 2.x 里HandlerInterceptorAdapter还没废弃,但更推荐直接实现接口,代码更干净。
public class SwaggerAuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(false); boolean loggedIn = session != null && session.getAttribute("swagger_login") != null; if (loggedIn) { return true; } String uri = request.getRequestURI(); if (uri.endsWith(".html") || uri.contains("/swagger-ui") || uri.equals("/")) { response.sendRedirect(request.getContextPath() + "/swagger-login.html"); } else { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"unauthorized\"}"); } return false; } }这里有一个关键的处理逻辑:当用户访问的是swagger-ui.html这种页面请求时,重定向到登录页是友好的;但当某个 AJAX 请求访问/v3/api-docs或/swagger-resources/configuration/ui时,如果也返回 302 重定向,Swagger 页面里的 JS 就会拿到一个登录页 HTML,解析 JSON 时直接报错。所以我对非页面请求返回 401 和 JSON,而不是重定向。这个问题后面在排坑部分还会细说。
2.3 注册拦截器并配置放行路径
有了拦截器光写在类里没用,必须注册进 Spring MVC 的拦截器链里。新建一个配置类:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SwaggerAuthInterceptor()) .addPathPatterns( "/swagger-ui.html", "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**", "/v2/api-docs/**", "/doc.html" ) .excludePathPatterns( "/swagger-login.html", "/swagger-login", "/css/**", "/js/**", "/images/**" ); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }注册拦截器时有三个细节值得注意。
第一,addPathPatterns必须把 Swagger 相关的资源都包含进去。如果你用的是 springfox,那么/swagger-resources/**是必须的,因为 Swagger 页面初始化时会请求这个路径获取分组信息;如果你用的是 springdoc,那么/v3/api-docs/**是必须的。漏掉任何一个,别人依然能通过接口地址直接拿到 JSON 格式的接口定义,那就等于登录页白做了。
第二,excludePathPatterns里必须放行登录页本身和登录接口。不然的话会形成“登录页也被拦截器拦住,还没法提交登录请求”的死锁。
第三,不要偷懒用addPathPatterns("/**")去拦所有路径。如果项目里还有其他业务接口也挂在同一个 SpringBoot 应用上,那所有业务请求都会被这个拦截器拦下来,影响面太大。按 Swagger 路径精确配置,是风险最小的做法。
2.4 编写登录接口与 Session 管理
登录接口负责校验账号密码并写入 session。我一般把用户名和密码放在配置里,这样测试环境用一套密码,生产环境可以单独覆盖。
@Controller public class SwaggerLoginController { @Value("${swagger.auth.username:admin}") private String username; @Value("${swagger.auth.password:admin123}") private String password; @PostMapping("/swagger-login") public String login(String username, String password, HttpSession session, HttpServletRequest request) { if (this.username.equals(username) && this.password.equals(password)) { session.setAttribute("swagger_login", true); session.setMaxInactiveInterval(8 * 60 * 60); return "redirect:" + request.getContextPath() + "/swagger-ui.html"; } return "redirect:/swagger-login.html?error=1"; } @GetMapping("/swagger-logout") public String logout(HttpSession session) { session.invalidate(); return "redirect:/swagger-login.html"; } }这里有个容易被忽略的地方:session.setMaxInactiveInterval(8 * 60 * 60),单位是秒,这里是设置 session 8 小时过期。如果开发一个大型项目,排查问题时频繁重新登录会让人崩溃,8 小时是一个比较合理的折中值。如果你希望 Swagger 会话“永不掉线”,也可以设置一个很长的过期时间,但生产环境建议还是控制在一到两个工作日内。
再一个细节是:登录失败时我用的是redirect:/swagger-login.html?error=1,然后在登录页通过 JS 判断 URL 参数显示错误提示。这样比用ModelAndView转发更简单,因为登录页是静态页面,没必要为了一个错误提示引入模板引擎。
2.5 登录页面的制作与资源放行
登录页我选择放在src/main/resources/static/swagger-login.html。SpringBoot 的静态资源默认映射路径就是/static下面的内容,所以访问/swagger-login.html时 SpringBoot 会自动返回这个文件。
页面本身不用搞太复杂,但要把“这是接口文档认证入口”的提示写清楚,避免同事忘了密码时一脸懵。
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>接口文档登录</title> <style> body { margin: 0; padding: 0; background: linear-gradient(135deg, #1e3c72, #2a5298); height: 100vh; display: flex; justify-content: center; align-items: center; font-family: "Microsoft YaHei", sans-serif; } .login-box { background: #fff; padding: 48px 56px; border-radius: 12px; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2); width: 360px; } .login-box h2 { text-align: center; margin-bottom: 8px; color: #1e3c72; } .login-box .sub-title { text-align: center; color: #888; font-size: 14px; margin-bottom: 32px; } .login-box input { width: 100%; height: 44px; box-sizing: border-box; margin-bottom: 16px; padding: 0 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; transition: border-color 0.2s; } .login-box input:focus { outline: none; border-color: #2a5298; } .login-box button { width: 100%; height: 44px; background: #2a5298; color: #fff; border: none; border-radius: 6px; font-size: 15px; cursor: pointer; transition: background 0.2s; } .login-box button:hover { background: #1e3c72; } .error-msg { color: #e74c3c; font-size: 13px; text-align: center; margin-bottom: 10px; display: none; } </style> </head> <body> <div class="login-box"> <h2>API 接口文档</h2> <div class="sub-title">请先认证后再访问</div> <div class="error-msg" id="errorMsg">用户名或密码错误</div> <form action="/swagger-login" method="post"> <input type="text" name="username" placeholder="用户名" required> <input type="password" name="password" placeholder="密码" required> <button type="submit">登 录</button> </form> <div style="text-align:center; margin-top: 16px;"> <a href="/swagger-logout" style="color:#999; font-size:12px; text-decoration:none;">退出登录</a> </div> </div> <script> if (location.search.indexOf('error=1') !== -1) { document.getElementById('errorMsg').style.display = 'block'; } </script> </body> </html>我把样式直接内联在这个 HTML 文件里,而不是单独放一个 CSS 文件。为什么?因为在拦截器场景里,静态资源的放行路径越多,出问题的可能性越大。如果你单独放一个login.css,就必须确保excludePathPatterns里放行了/css/**。然而有些配置里/css/**没有正确匹配,就出现登录页样式全丢的诡异现象。把样式内联,彻底绕开这个坑。
2.6 配置文件里加上账号密码
在application.yml中增加自定义配置项。这里的swagger.auth.username和swagger.auth.password就是登录凭证。密码不建议直接写明文在仓库里,最好通过环境变量或配置中心下发。
swagger: auth: username: admin password: ${SWAGGER_PASSWORD:admin123}如果你想把密码加密存储,可以用 Spring 的Environment配合 Jasypt 做解密,或者直接在登录 Controller 中比对 BCrypt 密文。我平时更推荐直接存一个 BCrypt 密文到配置里,这样就算配置文件泄露,明文密码也不会直接暴露。
@Value("${swagger.auth.password-hash}") private String passwordHash;比对逻辑:
if (this.username.equals(username) && passwordEncoder().matches(password, passwordHash)) { // 登录成功 }2.7 完整请求链路示例
把所有代码写完后的完整请求流程是这样的:
- 浏览器访问
http://localhost:8080/swagger-ui.html。 SwaggerAuthInterceptor被触发,检查 Session 中是否存在swagger_login。- 第一次访问时没有登录标记,拦截器向浏览器返回 302 重定向到
/swagger-login.html。 - 浏览器加载登录页。
- 输入用户名密码,POST 到
/swagger-login。 - Controller 校验通过,Session 写入
swagger_login属性,然后重定向回/swagger-ui.html。 - 拦截器再次检查时发现登录标记存在,放行。Swagger 页面正常渲染。
- Swagger 页面初始化过程中,JS 会请求
/swagger-resources/configuration/ui、/swagger-resources等接口,这些请求同样经过拦截器,但因为登录标记存在,全部放行。
整个过程无感且顺畅,唯一的体验差异是首次打开时多了一次页面跳转。
3. 不同版本 Swagger 与路径适配
3.1 springfox 2.9.2 的路径清单
如果你用的是经典的 springfox 2.9.2,swagger-ui.html背后包含这些资源:
/swagger-ui.html:页面入口/swagger-resources:返回所有 api-docs 分组信息/swagger-resources/configuration/ui:UI 配置/swagger-resources/configuration/security:安全配置/v2/api-docs:JSON 格式的接口定义
这些路径里的任意一个被直接访问,都能拿到接口文档信息。所以拦截的时候不能只拦swagger-ui.html一个路径,要把第 2.3 节里列出的那串路径全部包含进去。很多人在这一步图省事,只加了/swagger-ui.html,结果别人直接请求/v2/api-docs照样能看到全部接口定义,登录页形同虚设。
3.2 springdoc / OpenAPI 3 的路径变化
如果你的项目用的是 springdoc,依赖大致是:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>1.7.0</version> </dependency>这种情况下访问地址变成了http://localhost:8080/swagger-ui/index.html,对应的 JSON 接口是/v3/api-docs,资源配置路径是/v3/api-docs/swagger-config。
拦截器适配起来只需要把第 2.3 节里的addPathPatterns改为:
.addPathPatterns( "/swagger-ui/**", "/v3/api-docs/**" )如果你在项目中同时引入了 knife4j,那访问地址就是/doc.html,需要额外加一个拦截路径。knife4j 本质上还是基于 springdoc 或 springfox 的增强包,所以/doc.html、/swagger-ui/**、/v3/api-docs/**都要拦截。
3.3 SpringBoot 2.6+ 的路径匹配策略问题
SpringBoot 2.6 开始,默认的路径匹配策略从AntPathMatcher切换成了PathPatternParser。这导致一个很经典的坑:springfox 2.9.2 在 SpringBoot 2.6 以上的项目里,启动时直接报错,提示类似:
Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException解决方法是在配置文件中加一行:
spring: mvc: pathmatch: matching-strategy: ant_path_matcher这一行跟我们讨论的登录拦截器关系不大,但如果你在老的 springfox 项目上做登录功能,很可能同时会遇到这个启动问题,这里一并列出来,省得大家排查半天。
3.4 SpringBoot 3.x 与 Jakarta 命名空间迁移
如果你的项目已经升级到 SpringBoot 3.x,那么javax.servlet全部换成了jakarta.servlet。拦截器代码里 import 的HttpServletRequest、HttpServletResponse都要改成jakarta.servlet.http.*。依赖上springfox基本已经不再维护,升级后建议直接切换成 springdoc。拦截器整体逻辑不用动,路径适配参照 3.2 节即可。
这里还有一个小提醒:SpringBoot 3.x 中如果沿用老的HandlerInterceptorAdapter,这个类可能已经没了,必须直接实现HandlerInterceptor接口。我习惯直接实现接口,也是因为这样在版本升级时改动最小。
4. 生产环境开关与配置建议
4.1 用配置开关控制 Swagger 是否启用
登录页面只是第一步。在生产环境,更推荐的做法是通过配置直接关闭 Swagger。这通常需要两步:第一步,配置项控制Docket是否创建;第二步,配置项控制拦截器是否注册。
先看 Docket 的开关,使用 springfox 时这样的逻辑很常见:
@Configuration public class SwaggerConfig { @Value("${swagger.enabled:true}") private boolean enabled; @Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .enable(enabled) .select() .apis(RequestHandlerSelectors.basePackage("com.example.controller")) .paths(PathSelectors.any()) .build(); } }然后拦截器的注册也做一下开关:
@Override public void addInterceptors(InterceptorRegistry registry) { if (swaggerEnabled) { registry.addInterceptor(new SwaggerAuthInterceptor()) .addPathPatterns(...) .excludePathPatterns(...); } }这样在application-prod.yml里设置swagger.enabled: false,生产环境既没有文档页面,拦截器也不用注册。最省心的做法是:
# application-prod.yml swagger: enabled: false但要注意:即使 Swagger 停用,如果拦截器里还拦截着/swagger-ui.html,访问时可能返回 404 而不是 302,这是正常的。关掉 Swagger 后,这些路径本来就不该存在。
4.2 登录密码不要出现在代码仓库里
密码写在application.yml里虽然方便,但如果代码仓库是多人共享的,密码会跟着代码走。我的习惯是:默认密码写得无伤大雅,比如admin123,然后通过环境变量或者启动参数覆盖。这样开发环境开箱即用,正式环境用外部注入的强密码。
启动命令里可以这样指定:
java -jar app.jar --swagger.auth.password=$(cat /etc/secrets/swagger_pwd)或者更优雅一点,用环境变量:
export SWAGGER_PASSWORD='Xk#9pL2mQ!' java -jar app.jar4.3 失败次数限制与安全加固
如果 Swagger 暴露在公网,仅靠一个登录页其实还不够。攻击者可以暴力尝试弱口令。我的建议是加一个简单的失败次数限制。实现思路可以这样:用ConcurrentHashMap<String, Integer>记录 IP 对应的失败次数,超过 5 次后锁定 30 分钟。
@Component public class LoginAttemptService { private static final int MAX_ATTEMPT = 5; private static final long LOCK_DURATION = 30 * 60 * 1000L; private final ConcurrentHashMap<String, Integer> attempts = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, Long> lockTime = new ConcurrentHashMap<>(); public boolean isLocked(String key) { Long lockUntil = lockTime.get(key); if (lockUntil == null) { return false; } if (System.currentTimeMillis() > lockUntil) { attempts.remove(key); lockTime.remove(key); return false; } return true; } public void loginFailed(String key) { int count = attempts.merge(key, 1, Integer::sum); if (count >= MAX_ATTEMPT) { lockTime.put(key, System.currentTimeMillis() + LOCK_DURATION); attempts.remove(key); } } public void loginSuccess(String key) { attempts.remove(key); lockTime.remove(key); } }在登录 Controller 的校验前先判断是否被锁定,登录失败后调用loginFailed。
@PostMapping("/swagger-login") public String login(String username, String password, HttpSession session, HttpServletRequest request) { String ip = request.getRemoteAddr(); if (loginAttemptService.isLocked(ip)) { return "redirect:/swagger-login.html?lock=1"; } if (this.username.equals(username) && this.password.equals(password)) { loginAttemptService.loginSuccess(ip); session.setAttribute("swagger_login", true); return "redirect:" + request.getContextPath() + "/swagger-ui.html"; } loginAttemptService.loginFailed(ip); return "redirect:/swagger-login.html?error=1"; }这套方案成本低且有效,至少能挡住大部分脚本扫描式的暴力破解。如果你有更高的安全诉求,可以在此基础上加验证码,但作为内部开发文档的入口,失败次数限制已经够用。
4.4 多网关环境下的 Session 共享问题
如果你的项目部署了多个实例,并且前面挂了一层负载均衡,那么 Session 做到什么程度就很关键。默认情况下,用户登录之后,Session 存在某台实例上,下一次请求被负载均衡转发到另一台实例时,Session 就丢了,表现为“登录后没多久又跳回登录页”。
针对这种情况,要么配置 Spring Session 使用 Redis 做集中存储,要么在 Nginx 层做 IP_Hash 会话保持。我更推荐前者,因为 Spring Session 的整合成本极低,依赖加上之后只用配置一行即可:
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>spring: session: store-type: redis这样 Session 放在 Redis 里,多个实例共享同一份会话状态。不过如果你的项目规模没那么大,单机部署,这个坑可以先不用管。
5. 常见问题与排查技巧实录
5.1 登录页打不开,访问就报 404
最常见的原因是静态资源没放对位置。SpringBoot 的默认静态资源位置是classpath:/static/、classpath:/public/、classpath:/resources/等。你直接把swagger-login.html丢进resources/static/下面,访问/swagger-login.html才能正确返回。如果你把文件放到了resources/templates下面,又没有引入 Thymeleaf,那 SpringBoot 不会自动渲染这个页面,结果就是 404。
另外,项目如果重写了spring.mvc.static-path-pattern,比如统一加了一个前缀/assets/**,那你的登录页访问路径也会随之变化。一般项目很少改这个配置,但如果遇到访问 404,第一时间检查这个配置。
5.2 Swagger 页面始终重定向到登录页
这种问题一般发生在登录成功之后,Session 标记也已经写了,但 Swagger 页面还是不停跳回登录页。排查步骤:
- 先看浏览器开发者工具里请求
/swagger-ui.html时 Cookie 是否带上JSESSIONID。 - 再看登录成功后返回的 302 重定向地址里是否带有之前登录的路径上下文。
- 如果项目部署在 Tomcat 根路径下,通常没问题;如果部署在
/api这种子路径下,重定向时必须带request.getContextPath()。
最常见的坑是:登录成功后重定向到/swagger-ui.html,但项目部署在/demo下,实际要访问的是/demo/swagger-ui.html。我在代码里特别写了request.getContextPath(),就是为了防止这个情况。如果跳转地址少了 ContextPath,浏览器访问 404 或者被网关拦截,就会让人误以为登录没成功。
5.3 Swagger 页面能打开,但接口定义加载不出
这种情况比较隐蔽。登录后swagger-ui.html本身能出来,但页面中间的接口列表一直转圈加载不出来。按 F12 打开开发者工具,通常能在 Console 看到类似的错误:
Failed to load resource: the server responded with a status of 401 (Unauthorized)原因在于 Swagger 页面里的 JS 会请求/v3/api-docs或/swagger-resources这类异步接口。如果拦截器对这些请求返回的是重定向而不是 JSON,JS 就会解析失败。所以我在拦截器里做了处理:对.html后缀的页面请求重定向,对非页面请求返回401 + JSON。
这里有一个更细的坑:Springfox 的/swagger-resources/configuration/ui路径是不带.html的,如果你的拦截器把所有接口请求都重定向了,这个问题必现。正确做法就是严格按第 2.2 节的if (uri.endsWith(".html") || uri.contains("/swagger-ui"))来区分。
5.4 登录页.css 样式丢失,页面光秃秃的
登录页能打开,但是样式乱成一团。要么是我说的,你把样式写在独立 CSS 文件里但没放行/css/**;要么是静态资源配置冲突。我个人的建议很简单:登录页的样式直接内联写进 HTML,别折腾外部样式文件。登录页本来就不大,内联样式不会给代码维护带来多大负担,反而省去了一堆路径放行的麻烦。
5.5 每次访问都需要重新登录
检查两个维度:第一,server.servlet.session.timeout,如果超时时间设置得太短,比如默认的 30 分钟,那用户看文档看到一半,Session 过期,再点一下接口详情,就触发跳登录页了。可以适当调长,比如设为 8 小时:
server: servlet: session: timeout: 8h第二,如果你在前端框架里对 Swagger 页面做了请求代理,比如通过 Vite 或 Webpack proxy 转发,要确保代理配置没有修改 Set-Cookie 头。有时候是代理层把 Cookie 丢了,导致浏览器永远带不上 Session ID。
5.6 多实例部署时登录跳来跳去
这个问题在 4.4 节已经讲过。现象是:用户登录成功后跳回swagger-ui.html,有时候是正常的,有时候又跳回登录页,刷新一下又好了。这是因为负载均衡把请求分发到了不同的实例,而 Session 没共享。解决方案就是引入 Spring Session + Redis。如果你不想引入 Redis,也可以在网关层配置基于 IP 的会话保持,让同一个来源 IP 的请求始终命中同一台实例。两种方案都可行,我一般优先选 Spring Session,因为它对应用是透明的。
5.7 拦截器没有生效
如果你配置了拦截器但访问 Swagger 页面时毫无反应,先在配置类上确认有没有加@Configuration。再确认addInterceptors方法是不是被正确调用。还有一个很低级的坑:项目里如果存在多个WebMvcConfigurer实现,其中一个覆盖了另一个的拦截器定义。排查时可以在addInterceptors方法里打一条日志,确认是否执行。
另外一点,如果项目里接了网关或过滤器,比如公司自研的鉴权过滤器,也有可能在拦截器之前就返回了 401,导致 Swagger 登录页根本无法渲染。前提是要把 Swagger 放行给排除掉,或者把登录页路径也加入白名单。
5.8 SpringBoot 2.6 以上启动报错
前面提过,Springfox 2.9.2 在 SpringBoot 2.6 以上的启动问题会表现为:
Caused by: java.lang.NullPointerException: null at springfox.documentation.spring.web.WebMvcRequestHandlerProvider...解决方式就是在配置里加spring.mvc.pathmatch.matching-strategy: ant_path_matcher。如果你加了之后还报别的错,比如版本兼容问题,那就直接升级到 springdoc。其实从长期维护的角度看,springfox 停更已久,新项目建议直接用 springdoc,老项目在看得到收益的情况下也应该安排迁移。
6. 实际项目中的经验分享
写到这里,整个登录页加拦截器的实现已经完整了。最后分享几个我在实际项目里的体会。
第一,给 Swagger 加登录页,重量级一定是轻量级优先。很多团队一提到“安全”就想到 Spring Security,实际上一个内部接口文档入口不值得引入整个安全框架。拦截器能解决的问题,用拦截器解决,简单直白,后面的人也好维护。当然如果项目本身已经引入了 Spring Security,那就走它现成的过滤链,别重复造轮子。
第二,登录后的会话过期时间一定要按“开发场景”而不是“业务场景”来设计。业务系统的 Session 可能只想保持 30 分钟,但 Swagger 页面是用来调试接口的,项目结构复杂的话,光排查一个接口问题可能就要一两个小时。Session 时间设置太短,过程中不停跳登录页,非常影响开发体验。我一般把 Swagger 登录 Session 单独设置为 8 小时,甚至更长。
第三,密码别搞得太复杂,但也不能不设。如果 Swagger 只是内网开发环境用,密码做到“同事要用方便、外部猜不到”这个平衡就好。安全性要求更高的场景,再叠加 IP 白名单、失败次数限制。或者干脆一步到位,生产环境直接关闭 Swagger,测试环境才开启登录页,这是性价比最高的方案。
第四,如果项目里有多个微服务,每个服务都配一套 Swagger 登录页会显得很重复。可以考虑把拦截器、登录 Controller、登录页抽成一个公共 starter 或者公共模块,各个服务直接复用。SpringBoot 的 auto-configuration 机制做这个非常顺手,等你有三个以上服务再动手抽也不迟。
给swagger-ui.html加登录页,本质上是给接口文档套一层薄薄的认证壳。它不复杂,但涉及路径映射、Session、重定向、静态资源、版本差异等多个细节点,任何一个位置没处理好都会让你多踩几个小时的坑。希望这篇文章能帮你一次把事情做对,少走那些我走过的弯路。