Swagger 文档在本地开发时是好东西,但项目一旦部署到测试环境或者公网服务器,直接裸奔不设防,那就等于把接口结构、字段定义、甚至生产环境的数据库表信息全给交出去了。我自己就见过不少团队把 Swagger-UI 直接暴露到线上,被爬虫扫到之后数据接口被刷爆,最后只能紧急下线版本。给 swagger-ui.html 加一个登录页面挡在门口,是成本最低、见效最快的一种保护手段。
这篇文章我会基于 SpringBoot 项目里最常见的三种做法展开:第一,用 Spring Security + 默认登录页做内存认证;第二,用原生 Filter / 拦截器 做一次轻量级鉴权;第三,结合 Vue3 前后端分离项目时,登录页面联动与 API 文档保护的思路。顺带把 SpringBoot 2.x 和 3.x 的配置差异讲清楚,因为这个问题让不少人踩过坑。
1. 项目场景拆解:为什么 Swagger 文档必须加一道登录门
1.1 标题背后反映的典型痛点
先把这个需求背后的实际场景拆开看。标题里明确写了“SpringBoot项目 访问 swagger-ui.html 添加登录页面”,这在真实项目里就是一句话:我已经把接口文档集成到项目里了,但不想让任何拿到 URL 的人直接看到接口文档内容。这个诉求一般出现在三类环境里:
- 开发环境:联调时前端、测试、后端多人共享接口信息,但不是所有人都应该看到全部接口,也方便记录谁在什么时间访问过文档。
- 测试环境:测试同学要验证接口,但是接口文档不该暴露给外部无关人员。
- 生产环境:这种场景比较紧急,如果 Swagger 已经不小心带上线了,加一道登录页面是给系统“补漏”的兜底操作。
很多初学 SpringBoot 的开发者会把注意力放在“如何把 Swagger 集成进项目”上,却很少思考“如何控制 Swagger 谁能看”。实际工作中,接口文档暴露的影响面比大部分人想象的严重得多。我记得有一次帮一个朋友排查问题,他的项目在测试服务器上被第三方拿 Swagger 文档扫描之后,直接对着接口文档里的字段名做了批量试探请求,虽然没造成数据丢失,但日志里多了几千条异常记录,排查起来非常头疼。
1.2 直接使用默认登录表单还是自建登录页
给 swagger-ui.html 加登录页,第一个要明确的方案选择是:用 Spring Security 框架自带的默认表单登录,还是自己写一个登录页面。
前者改造量很小,Spring Security 会自动渲染一个默认的登录页面,输入用户名和密码之后通过 Session 维持登录态,然后允许访问受保护的资源路径。后者需要自己写登录页 HTML、登录接口、会话管理逻辑,适合对登录页样式有要求、或者项目本身已经有一套用户体系的场景。
我给出的建议是:如果只是为了“挡住不该看的人”,用 Spring Security 默认登录页就够了;如果项目里已经存在用户表,那可以引入 Spring Security 并自定义 UserDetailsService,让 Swagger 文档的访问控制复用现有账号体系。这篇文章里会先讲基于内存用户的极简打法,因为这是最快解决“裸奔”问题的方式。
提示:这篇文章里的代码案例基于 Spring Boot 2.7.x 和 Spring Boot 3.x 两个版本分别说明,两个版本之间的 Security 配置写法差异很大,后面会单独拿一节来讲,避免你对着新版本的代码去搜老版本的教程,越看越糊涂。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 极简方案初体验:Spring Security 默认登录页保安全
2.1 引入依赖与版本选择
如果你用的是 Spring Initializr 创建的项目,引入 Spring Security 只要加一个依赖。Maven 项目在 pom.xml 里加这一段:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Gradle 项目就在 build.gradle 的 dependencies 里加:
gradle复制implementation 'org.springframework.boot:spring-boot-starter-security'
这里有一个非常关键的版本背景:Spring Boot 2.7.x 系列默认对应的 Spring Security 是 5.7.x,而 Spring Boot 3.x 对应的 Spring Security 是 6.x。两者之间的配置方式发生了巨大变化,其中最核心的变化是 WebSecurityConfigurerAdapter 这个类被彻底废弃了。早期网上流传的大部分 Spring Security 教程都在继承这个类,如果你拿 SpringBoot 3.x 项目去跑那些老代码,会发现编译直接报错。
热词里也提到了“springboot版本太高”这种说法,本质上就是版本升级之后,旧写法失效带来的学习成本。我的实际经验是:学习阶段尽量把 SpringBoot 版本固定在 2.7.x 或者 3.2.x,不要追最新。2.7.x 的社区资料最多,3.2.x 的写法更贴近未来趋势,但踩坑会多一些。
2.2 极简配置类:全局保护与路径放行
引入依赖之后,什么都不配置的情况下,Spring Security 会默认把项目里所有接口都保护起来,包括 Swagger 文档页面。启动时控制台会打印一串随机密码,配合默认用户名 user 才能登录。这其实已经达到了“访问 swagger-ui.html 需要登录”的效果,但它有一个副作用:你的业务接口也全部需要登录才能访问,这显然不是我们想要的。
所以需要自己写一个配置类,把需要放行的路径放行,把需要保护的路径保护起来。先给一个 Spring Boot 2.7.x 时代比较经典的写法:
java复制import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder.encode("admin123"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(admin);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// 放行 swagger 相关的静态资源,这样页面本身能加载
.antMatchers(
"/swagger-ui.html",
"/swagger-ui/**",
"/v3/api-docs/**",
"/swagger-resources/**",
"/webjars/**"
).authenticated()
// 其余请求全部放行,不拦截业务接口
.anyRequest().permitAll()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.csrf(csrf -> csrf.disable())
.httpBasic(with -> {});
return http.build();
}
}
这段配置达到了几个效果:
- 访问
/swagger-ui.html以及 Swagger 页面背后加载的静态资源时,必须先登录。 - 业务接口都放行,不会影响前后端联调。
- 登录方式有两种:表单登录,以及 HTTP Basic 认证。
- 用户信息放在内存里,用户名
admin,密码admin123,后续可以换成从数据库读取。
注意一个细节:/swagger-ui.html 这个路径本身是静态页面的入口,Swagger 页面在浏览器里加载时,还会去请求 /swagger-ui/index.html、/v3/api-docs、/swagger-resources 等路径。如果只保护 /swagger-ui.html,而放行了 /v3/api-docs/**,那么别人虽然看不到 UI 页面,但还是能直接拿 /v3/api-docs 的 JSON 数据。所以保护的时候必须把相关的接口文档数据路径一起保护起来,这一点特别容易漏。
2.3 HTTP Basic 认证的取舍
上面配置里顺手加了一行 httpBasic,这个操作容易被忽略,但实际很有用。因为 Swagger UI 页面在做一些调试请求时,需要带着认证信息去访问受保护的 API 文档接口。如果只开启表单登录,浏览器里登录状态是没问题的,但在 Postman、Apifox 这类工具里调试时,就要手动加 Session Cookie,稍微麻烦一些。
开启 HTTP Basic 之后,工具里只要在 Authorization 里填入 admin / admin123,就能直接访问文档接口。我自己在调试时基本是表单登录和 Basic 认证同时开的,灵活很多。当然,如果你的项目对安全性要求很高,Basic 认证因为每次请求都会携带明文 Base64 编码的用户名密码,建议配合 HTTPS 一起使用,否则不建议在生产环境开启。
3. 登录页面的动态交互:从默认页到自定义登录页
3.1 为什么默认登录页不够用
Spring Security 自带的默认登录页非常朴素,就是一个居中的表单,带一个 CSRF 隐藏字段。它能用,但有两个问题:
第一,样式和项目整体风格不搭。前后端分离的项目里,前端页面通常是 Vue3 或 React 构建的,登录页风格、字体、布局都经过设计,突然跳出一个浏览器默认样式的登录框,用户体感很差。
第二,默认登录页不支持品牌信息、验证码、滑块验证等扩展。热词里提到了“带滑块验证的登录页面如何模拟登录”,这其实就是一个典型需求:登录页希望加入滑块验证、行为校验这类机制,来防止机器脚本破解。
所以在项目里,我更推荐的做法是:使用静态登录页替换默认登录页,或者直接用自己的登录接口替换 Spring Security 的认证流程。下面先讲一个低成本的自定义登录页方案。
3.2 用静态 HTML 替换默认登录页
Spring Security 允许通过 loginPage() 指定一个登录页 URL,但它对登录页的提交参数名有约定:默认表单里用户名输入框的 name 必须是 username,密码输入框的 name 必须是 password。如果你的页面字段名不是这两个,需要额外通过 usernameParameter() 和 passwordParameter() 指定。
一个最简单的做法是在 /resources/static/custom-login.html 放一个静态页面,然后调整配置:
java复制.formLogin(form -> form
.loginPage("/custom-login.html")
.loginProcessingUrl("/doLogin")
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/swagger-ui.html")
.permitAll()
)
此时访问 Swagger 文档时,会自动跳转到 /custom-login.html,输入账号密码提交到 /doLogin,认证成功后回到 Swagger 页面。这个页面的样式可以自由设计,包括背景图、公司 Logo、第三方登录按钮等,完全由前端自由发挥。
这里有个细节需要说明:一旦指定了自定义 loginPage,Spring Security 就不再为你渲染默认登录页,也不会再替你处理登录页的 GET 请求之外的东西。你需要保证这个页面能被浏览器正常加载,所以要在配置里放行 /custom-login.html。同时,loginProcessingUrl 也必须记得在 authorizeHttpRequests 里配置 permitAll(),否则会陷入登录页面重定向死循环。
3.3 Vue3 动态背景登录页背后的小套路
热词里出现的“vue3 登录页面 点线动态的背景”是我比较熟悉的一种视觉风格。Vue3 项目里常见的登录页动态点线背景,本质上是用 Canvas 实现粒子连线动画:页面上生成几百个随机点,点与点之间如果距离小于某个阈值就画一条线,点本身缓慢移动,形成一种带有科技感的动态背景。这个效果看起来复杂,实现起来反而比较简单,核心逻辑就是三件事:
- 用
requestAnimationFrame做循环动画。 - 每帧更新粒子的位置,碰到边界反弹。
- 遍历粒子对,计算两点距离,小于阈值时用
ctx.strokeStyle画一条半透明直线。
这个效果放在 Swagger 登录页里也能玩,只要把自定义登录页做成 Vue3 单页静态构建产物,放到 SpringBoot 的 resources/static/custom-login/ 目录下,配置好 loginPage 就能用。不过如果只是个人小项目,其实没必要上 Vue3,直接一个 HTML 文件加几十行 JavaScript 也能实现同样的粒子背景。
4. 改造升级:从内存用户到数据库用户
4.1 内置用户体系无法满足真实项目
内存用户方案适合演示和极简场景,但真实项目里会有这么几个需求:
- 多个工程师共用一套文档账号,账号交付给别人后无法单独撤销。
- 想看操作日志,知道谁在什么时候访问过接口文档。
- 用户密码要支持修改,不能每次改完都重新打包部署。
这三点里的任何一点,内存用户方案都做不到,所以需要把用户信息接入到数据库。用 Spring Security 做这件事有两种常见路径:
路径一:实现 UserDetailsService 接口,重写 loadUserByUsername(String username) 方法,在这个方法里查数据库、封装 UserDetails 返回。
路径二:直接使用 MyBatis-Plus 或 JPA 查出用户信息,在 Controller 里自己做登录逻辑,不走 Spring Security 的过滤器链。
如果是全新项目,我建议走路径一,理由很直接:Spring Security 默认的认证流程非常成熟,自带 Session 管理、CSRF 防护、密码加密等能力,不重复造轮子。下面是一个基于 MyBatis-Plus 的最小实现。
4.2 基于 MyBatis-Plus 的 UserDetailsService
先写实体类和 Mapper,假设数据库里有一张 sys_user 表:
java复制import com.baomidou.mybatisplus.annotation.TableName;
@TableName("sys_user")
public class SysUser {
private Long id;
private String username;
private String password;
private Integer status;
// getter、setter 省略
}
Mapper 继承 BaseMapper<SysUser> 即可,然后实现 UserDetailsService:
java复制import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class DbUserDetailsService implements UserDetailsService {
private final SysUserMapper sysUserMapper;
public DbUserDetailsService(SysUserMapper sysUserMapper) {
this.sysUserMapper = sysUserMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserMapper.selectOne(
new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username)
);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles("ADMIN")
.build();
}
}
然后在配置类里,把前面 InMemoryUserDetailsManager 换成我们自己写的 DbUserDetailsService:
java复制@Bean
public UserDetailsService userDetailsService() {
return new DbUserDetailsService(sysUserMapper);
}
这样用户名密码就完全落到数据库里了,密码字段存的是 BCrypt 加密后的密文。新增文档查看账号只需要往数据库里插一条记录,撤销账号只需要改状态位。
注意:数据库里存密码时千万不要存明文,至少在代码里用
BCryptPasswordEncoder加密后再入库。一个偷懒的做法是在项目里写一个临时启动类,调用passwordEncoder().encode("明文密码")生成密文,然后手动把密文 update 到数据库里。
4.3 自定义登录页与数据库认证串联
自定义登录页 + 数据库认证合在一起时,流程是这样的:
- 访问
/swagger-ui.html,未登录时被 Spring Security 拦截。 - 跳转到自定义登录页
/custom-login.html。 - 用户提交用户名密码到
/doLogin。 - Spring Security 过滤器链里的
UsernamePasswordAuthenticationFilter捕获请求。 - 调用
AuthenticationManager,内部通过DaoAuthenticationProvider调用DbUserDetailsService.loadUserByUsername()查询用户并比对密码。 - 认证通过后,Session 里写入认证信息,重定向到
/swagger-ui.html。 - 后续携带 Session Cookie 访问 Swagger 相关路径,直接放行。
这个链路清晰自然,没有绕路。要调试某一步是否成功,可以打开浏览器的开发者工具,查看网络请求里登录接口的响应状态码。如果返回 302,说明认证通过开始跳转了;如果返回 401 或 200 但页面不跳转,基本就是用户名密码错误或者 CSRF 问题。
5. 双重保障:给 Swagger 文档访问加操作日志
5.1 为什么需要记录谁看了接口文档
给 Swagger 文档加登录,很多人以为做到“能挡人就结束”,但我在实际项目里还加了一层:访问日志。原因很现实,只需要一个反例:某天项目接口被刷了,你翻日志发现终端 IP 是内网地址,但排查到具体是哪个同事、哪个账号访问过文档时,如果没有访问日志,只能大海捞针。
因为 Swagger 文档本身就是敏感信息的集合,谁打开过、什么时候打开过、看了哪些接口,这些信息在合规审计里是有价值的。实现方式有两种,一种是利用 Nginx 访问日志按 IP 去统计,另一种是在 SpringBoot 项目里写一个过滤器或拦截器,专门记录访问 Swagger 相关 URL 的日志。
5.2 写一个轻量过滤器记录 Swagger 访问痕迹
我比较推荐用 OncePerRequestFilter,它是 Spring 提供的一个过滤器基类,确保请求只被过滤一次。实现思路如下:
java复制import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;
@Component
public class SwaggerAccessLogFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(SwaggerAccessLogFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String uri = request.getRequestURI();
if (uri.contains("/swagger-ui") || uri.contains("/v3/api-docs") || uri.contains("/swagger-resources")) {
String username = request.getUserPrincipal() == null ? "anonymous" : request.getUserPrincipal().getName();
String ip = getClientIp(request);
log.info("[Swagger Access] time={}, user={}, ip={}, uri={}", LocalDateTime.now(), username, ip, uri);
}
filterChain.doFilter(request, response);
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
} else {
ip = ip.split(",")[0].trim();
}
return ip;
}
}
这里有一点要注意:request.getUserPrincipal() 在用户未登录时返回 null,已登录时返回认证对象。所以日志里能区分出匿名访问和登录后访问。如果在 Spring Security 配置里把 Swagger 路径设置成 authenticated(),理论上未登录用户到不了这个过滤器,但多写这一层判断没有坏处,因为万一以后有人把配置改成了 permitAll(),日志依然能兜底。
日志输出到控制台只是第一步,实际部署时建议配置 Logback 或 Log4j2,把 Swagger 访问日志单独输出到一个文件里,比如 logs/swagger-access.log。这样分析的时候用 grep 按用户名或者 IP 筛,效率高很多。
6. SpringBoot 3.x 版本下的配置差异与踩坑实录
6.1 新版写法与 lambda-DSL 的调整
SpringBoot 3.x 出来之后,最坑的一点就是安全配置的写法不同了。前面第 2 节里给的配置是基于 Spring Boot 2.7.x 的,直接搬到 3.x 会报错。主要的差异有两个:
第一个差异是 authorizeHttpRequests 替代了旧的 authorizeRequests。在 2.7 里用 antMatchers,在 3.x 里要改成 requestMatchers:
java复制http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/swagger-ui.html", "/swagger-ui/**").authenticated()
.anyRequest().permitAll()
);
第二个差异是 WebSecurityConfigurerAdapter 彻底不能用了。3.x 要求用 SecurityFilterChain Bean 的方式定义安全规则,这个在第 2 节里已经展示了,但 3.x 下默认配置还要求显式声明 UserDetailsService,否则启动时会自动生成默认用户密码。
6.2 3.x 版本可用的完整配置示例
这里给一个适用 Spring Boot 3.2.x 的完整配置类,可以直接复用:
java复制import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
var user = User.builder()
.username("admin")
.password(encoder.encode("admin123"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/swagger-ui.html", "/swagger-ui/**").authenticated()
.requestMatchers("/v3/api-docs/**", "/swagger-resources/**").authenticated()
.anyRequest().permitAll()
)
.formLogin(form -> form
.loginPage("/custom-login.html")
.loginProcessingUrl("/doLogin")
.defaultSuccessUrl("/swagger-ui.html")
.permitAll()
)
.logout(logout -> logout.logoutSuccessUrl("/custom-login.html"))
.csrf(csrf -> csrf.disable())
.httpBasic(with -> {});
return http.build();
}
}
说一个我实际遇到过的情况:在 SpringBoot 3.x 下指定自定义 loginPage 时,如果没放行 /error 路径,登录失败跳转时经常会返回一个白页面,因为 Spring Boot 的 /error 页面被安全策略拦截了。解决方式是在 authorizeHttpRequests 里加一行:
java复制.requestMatchers("/error").permitAll()
这个坑很少有人在教程里提,但遇到的人是真多。
6.3 版本升级后的兼容性检查清单
如果你正在把一个老项目从 SpringBoot 2.x 升级到 3.x,建议按照下面的清单逐项检查:
javax.servlet包名是否已经替换成jakarta.servlet。- 配置类里是否还引用了
WebSecurityConfigurerAdapter。 antMatchers是否全部改成了requestMatchers。- 安全配置里是否新增了
UserDetailsServiceBean 或对应配置。 - Swagger 相关依赖是否兼容 Spring Boot 3.x,目前
springdoc-openapi2.x 版本支持得比较好。 - 不需要安全保护的静态资源路径,如
/js/**、/css/**、/images/**,要记得放行。
我见到过不少升级到 SpringBoot 3.x 之后,Swagger 页面样式丢失的案例。原因就是静态资源路径被安全过滤链拦截了,配置里放行了 /swagger-ui/**,但没放行 /webjars/** 和部分静态资源路径,导致 HTML 能加载但 CSS、JS 全部 404。遇到这种问题,先拿浏览器开发者工具看 Network 面板,哪些静态资源返回 403,就在安全配置里把对应路径加入 permitAll() 或 authenticated(),问题基本能定位。
7. 前后端分离场景:Vue3 + SpringBoot 如何协同保护 Swagger
7.1 前后端分离之后,登录状态怎么管理
很多现代项目已经是前后端分离架构:前端用 Vue3 构建,部署在 Nginx 上,后端 SpringBoot 只提供 API。此时前端并不是直接访问 /swagger-ui.html,而是访问 Swagger 文档的地址时,往往会有几种做法:
- 做法一:前端站点和后端 API 在同一个域名下,通过 Nginx 反向代理。这种场景下,Spring Security 的登录逻辑和前面一样,前端访问
/swagger-ui.html时会被重定向到登录页。 - 做法二:前端站点在 8080 端口,后端 API 在 9090 端口,域名不同。这种情况下涉及跨域,Spring Security 默认会拦截 OPTIONS 预检请求,需要额外配置 CORS。
- 做法三:前端完全独立部署,Swagger 只在后端 API 地址上暴露,登录也直接走后端地址,不进前端站点。这种场景最省事,前端连登录页都不需要做,直接用后端自定义登录页或者默认登录页即可。
实际项目里,我遇到最多的组合是“前端 Vue3 + 后端 SpringBoot 前后端分离”,Swagger 文档放在后端地址直接访问。这种情况下,前端和 Swagger 是两套独立的入口:前端业务接口通过前端的登录逻辑做认证,通常是用 JWT;Swagger 文档用后端的 Spring Security 做会话认证。两者互不干扰。
7.2 同一端口下的路径区分策略
如果你的前端构建产物直接打进了 SpringBoot 的 resources/static 目录,比如部署成单体应用,那么前端页面和后端 API 同源,此时 Swagger 的登录保护和前端页面路由就需要格外区分。原理是:前端路由往往以 /#/ 或 /page/ 开头,Swagger 文档路径是 /swagger-ui.html,在安全配置里可以对这两类路径分别设置策略。
常见的做法是:
/swagger-ui.html、/swagger-ui/**、/v3/api-docs/**这些 Swagger 路径全部authenticated()。/api/**业务接口按照项目原来的方式鉴权。- 前端页面入口路径
/、/index.html、/assets/**全部permitAll(),因为前端页面本身不敏感,真正敏感的是页面背后的接口数据。
不要一上来就把全部路径设为 authenticated(),否则前端页面首次加载就会跳登录页,看起来逻辑没问题,但实际使用中会碰到很多静态资源被拦截导致的样式错乱,排查起来性价比很低。
7.3 前后端联调时 Swagger 调试接口的鉴权细节
用 Swagger UI 调试接口时,它本质上就是一个浏览器里的 HTTP 客户端,每个调试请求都会带上当前页面域名下的 Cookie。因此,只要你在浏览器里打开 Swagger UI 时是通过登录页认证过的,那么后续在 Swagger UI 里点击 Try it out 发送请求也会自动携带 Session Cookie,认证信息不会丢。
但有一种情况容易出问题:前端项目里如果配了独立的请求库,比如 axios,给业务接口请求加了一个单独的拦截器,去自动附带 Token 而不是 Cookie,那 Swagger UI 里的接口调试用的还是 Cookie 会话,两者之间互不干扰,不会出现“前端登录了,但 Swagger 里还要再登录一次”的情况,因为它们本来就是两套认证体系。
如果实在不想让 Swagger 文档的调试请求依赖 Cookie,也可以开启 HTTP Basic 认证,然后在 Swagger UI 页面的全局参数里塞入 Authorization 头。有些团队会让 Swagger 支持从请求头里读取认证信息,这在多端调试时体验更好一点。
8. 常见问题与排查技巧实录
8.1 登录后仍然无法访问 Swagger 页面
这个问题出现的频率非常高。排查顺序我建议是这样的:
- 打开浏览器开发者工具,查看地址是否停留在
/login或/error。 - 观察登录请求的响应码,如果是 403,大概率是 CSRF 没关闭,而你的自定义登录页又不带 CSRF Token。解决办法是在 Security 配置里临时
csrf.disable()。 - 观察跳转后的地址,如果跳到了
/swagger-ui.html但页面白屏,看 Network 里有没有资源 403,如果有,说明静态资源路径没有放行。 - 如果显示 404,确认项目里是否真的集成了正确的 Swagger 依赖,很多时候 SpringBoot 3.x 项目需要用
springdoc-openapi-starter-webmvc-ui,而不是老的springfox。
8.2 放行了所有路径,登录页却一直转圈
这种情况一般是自定义登录页路径没有 permitAll(),导致登录页资源本身也需要认证,而认证失败又会跳回登录页,形成无限循环。用浏览器抓包能看到一个现象:请求 /custom-login.html 返回 302,Location 还是 /custom-login.html。
解决方式很简单:
java复制.requestMatchers("/custom-login.html", "/doLogin").permitAll()
另外如果登录页里引用了外部 CSS 或 JS,也要一并放行。
8.3 静态资源被 Security 拦截导致样式异常
Swagger 页面样式异常绝大多数是 /webjars/** 路径被拦截导致的。Spring Boot 3.x 下 Swagger UI 页面会加载 /webjars/** 下的资源。在 Security 配置中加一行:
java复制.requestMatchers("/webjars/**").permitAll()
如果页面里还有其他静态资源,写完配置后先用浏览器直接访问那个路径,确认状态码是 200 再刷新页面。
8.4 前后端分离跨域时登录失败
前后端分离并且前端端口与后端端口不一致时,登录请求会跨域。浏览器会先发一个 OPTIONS 预检请求,如果 Spring Security 把 OPTIONS 也拦截了,前端会报 CORS 错误。处理方式是在安全配置里加上 CORS 配置,或者放行 OPTIONS 方法:
java复制http.cors(with -> {})
同时在后端配置一个 CorsConfigurationSource,指定允许的来源、方法和请求头。请求头里必须放行 Authorization、Content-Type 等字段,否则登录接口请求头被浏览器拦掉,一样登录不了。
8.5 密码加密导致登录失败
改为数据库存储用户密码后,经常出现密码正确但登录失败的情况。90% 的原因是数据库里存的密码是明文,而 DaoAuthenticationProvider 默认使用 BCrypt 校验。解决办法是重新对密码做 BCrypt 加密后入库。可以用命令行快速生成密文:
java复制public class PasswordGenerator {
public static void main(String[] args) {
System.out.println(new BCryptPasswordEncoder().encode("admin123"));
}
}
把生成的密文直接复制更新到数据库里,再重新登录就正常了。
9. Filter 方案对比:不用 Spring Security 能不能实现
9.1 原生 Filter 实现登录校验
如果项目里完全不想引入 Spring Security,也可以用原生 Servlet Filter 来实现。它的核心逻辑是:拦截 /swagger-ui.html 等路径,检查 Session 里是否存在登录标记,如果没有就重定向到登录页;登录页提交后校验账密,成功则写入 Session。
好处是依赖少、逻辑透明,适合对 Spring Security 不熟或者不想引入一堆自动配置的项目。坏处是 Session 管理、密码加密、CSRF 防护这些都要自己写,而且后面如果其他路径也要求鉴权,这套 Filter 很难复用,基本上要自己再抽一层框架。
我给出的判断标准是:如果只是为了给 Swagger 文档加登录,原生 Filter 完全够用;如果项目后续还有接口鉴权需求,直接用 Spring Security 更合适,省得后面推倒重来。
9.2 最简单的 Session 校验 Filter
下面是一个原生 Filter 的简化思路,代码能跑但生产环境需要继续完善:
java复制@Component
public class SimpleAuthFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String uri = req.getRequestURI();
boolean swaggerPath = uri.contains("/swagger-ui") || uri.contains("/v3/api-docs") || uri.contains("/swagger-resources");
if (swaggerPath) {
Object loginUser = req.getSession().getAttribute("loginUser");
if (loginUser == null) {
resp.sendRedirect("/custom-login.html");
return;
}
}
chain.doFilter(request, response);
}
}
配合一个简单的登录 Controller,账密固定或查数据库都行,登录成功后在 Session 里放入 loginUser 属性。这个方案的坑在于:过滤器要注册到 Spring Boot 的过滤器链里,并且要避免拦截到登录接口本身上,否则又是死循环。
我建议如果你真的想走 Filter 路线,把 Swagger 相关路径的都写在一个常量数组里统一管理,以后加新路径只需改一处,维护成本低一点。
9.3 三种方案对比如表
为了让你选型时有清晰参照,我把三种方案放在一起对比:
| 方案 | 改造量 | 安全性 | 可扩展性 | 适用场景 |
|---|---|---|---|---|
| Spring Security 默认登录 | 低 | 高 | 高 | 大多数新项目,强烈推荐 |
| 静态/动态自定义登录页 | 中 | 高 | 高 | 对登录页样式有要求时 |
| 原生 Filter 拦截 | 中 | 中 | 低 | 不想引入 Security 的轻量项目 |
从长期维护角度看,Spring Security 上限更高,团队里如果有人熟悉它,后期加权限控制会很顺手。如果团队没有专门的安全开发经验,但又不想用框架,原生 Filter 也能兜住,但别指望它帮你挡住复杂攻击。
10. 多环境配置与部署时的账号安全
10.1 同一套代码,不同环境不同账号
日常项目至少会区分开发环境、测试环境和生产环境,Swagger 文档账号不应该在所有环境都一样。生产环境的账号尤其要注意与开发测试环境区分开。利用 SpringBoot 的配置文件机制,可以做到这点。
在 application-dev.yml 里设置一个 swagger.username 和 swagger.password,在 application-prod.yml 里设置另一组值,然后通过 @Value 注入:
java复制@Value("${swagger.username}")
private String swaggerUsername;
@Value("${swagger.password}")
private String swaggerPassword;
在配置类里用这两个变量构造用户信息。这样即使同一个 Jar 包,在不同 Profile 下启动,账号密码也会自动跟着环境走。
10.2 配置文件里的密码不要明文出现
明文密码写进 application.yml 有一个风险:如果代码仓库泄露或者代码被拖走,账号密码同时暴露。一个简单的处理方式是引入环境变量:
yaml复制swagger:
username: ${SWAGGER_USERNAME:admin}
password: ${SWAGGER_PASSWORD:}
这样默认情况下生产环境的密码为空,启动时必须由运维在服务器环境变量里注入 SWAGGER_PASSWORD,代码仓库里完全没有敏感信息。这种方式简单实用,我自己的项目基本都这么配置。
10.3 生产环境关闭 Swagger 的开关
除了加登录,更安全的做法是在生产环境直接关闭 Swagger 开关。用 springdoc 时,在配置文件里设置:
yaml复制springdoc:
api-docs:
enabled: false
swagger-ui:
enabled: false
在 application-prod.yml 里把这两项设为 false,生产环境直接不暴露文档,测试环境再打开。这跟加登录页并不冲突,相当于上了双重保险:第一层直接不提供文档服务,第二层就算有人把配置改回来,密码还在挡着他。
我在实际项目里给客户的建议也是:生产环境优先关闭文档,测试环境用登录页保护。因为生产环境里开文档不仅是安全问题,还会占用一点内存和部分请求路径,能关就关。
11. 我的一些实操心得与踩坑记录
11.1 优先级排序:先挡住,再优化体验
做这类安全改造时,我建议不要一开始就想着把登录页做到多好看、交互多流畅,先把“访问 swagger-ui.html 时必须登录”这个底线守住,然后再考虑自定义页面的样式、数据库账号这些优化项。
原因很简单:在一个已经运行的项目里,每次改动都有风险。Spring Security 的过滤器链如果配置不对,可能会导致所有接口 403,影响业务。先用最小改动把门锁上,再慢慢打磨门面,这样的节奏对团队来说最安全。
11.2 给 Swagger 单独开一个接口文档账号
另一个非常实用的经验是:给你的 Swagger 文档单独建一个数据库账号,不要直接拿管理员账号或者业务账号来登录文档。这个账号可以没有业务权限,只能查用户表、看文档。因为 Swagger 本身就是暴露接口信息给团队协作的,没必要把权限开太大。如果以后要撤销某人的文档访问权限,只需要把这个文档账号禁用,不会影响他正常的业务系统登录。
11.3 定期翻翻 Swagger 访问日志
在给 Swagger 加上访问日志之后,养成定期查看的习惯。正常情况下,访问 Swagger 的 IP 应该是自己团队的网段或者办公网 IP。如果日志里频繁出现陌生的 IP 段、凌晨时段的访问记录,就要留意是不是有人在扫你的接口文档。这个时候不要慌,先去 Nginx 层或者防火墙层把陌生 IP 拉黑,再考虑是否需要把 Swagger 文档关闭。
我遇到过最典型的一次情况是:某个测试环境没有做任何限制,Swagger 文档开了一个多月,日志里累计了三百多个陌生 IP 的访问记录,大多是扫描器留下的。好在只是访问文档,没有造成实际破坏,但这是一次很好的安全警示。从那以后,我接手任何 SpringBoot 项目,第一件事就是检查 Swagger 文档是否裸奔。
11.4 登录页之外的最后一层兜底
最后再分享一个小技巧:即使加了登录页,我也建议在 Swagger 文档页面的接口列表里,把那些特别敏感的操作接口用注解显式标记出来。springdoc 支持在每个接口上写描述,比如“生产环境谨慎调用”“仅限内网使用”。这样一来,即使有人登录了文档,也不会误操作或滥用一些危险接口。文档是给人看的,在文档里把边界画清楚,也是一种低成本的安全意识传递。
整个 Swagger 登录保护改造做下来,其实关键点不在于代码多复杂,而在于你是否意识到“文档暴露”这件事本身的风险。把访问控制加上、把敏感信息藏好、再留一条日志可追踪,这套组合拳打下来,接口文档才算真正可控。
