先说结论:给 SpringBoot 项目的 swagger-ui.html 加登录页面,真正费时间的不是写登录接口,而是搞清楚"到底要拦哪些路径、放行哪些资源"。我见过不止一个项目,辛辛苦苦把登录页和拦截器写好,部署到服务器上之后,要么登录页样式全丢,要么有人绕过入口直接拿接口文档,要么 Spring Boot 一升级 Swagger 直接启动失败。这篇文章就把这件事从头到尾拆一遍:方案怎么选、Swagger 的静态资源链路是什么、拦截器怎么实现、Spring Security 怎么做、以及那些年报上查不到但百分之百会踩的坑。
先说清楚,不是所有项目都需要这一步。开发环境自己本地调试,Swagger 裸奔没有任何问题;但一旦项目打成 jar 包部署到测试服务器、生产服务器,http://服务器IP:端口/swagger-ui.html 谁都能打开,意味着系统的全部接口路径、入参出参、接口说明全都暴露在公网上,这相当于把系统说明书贴在了大门上。给 Swagger 加登录页,本质就是给这份说明书加一道门禁。
1. 先想清楚方案:拦截器、Filter 还是 Spring Security
1.1 为什么"加个登录页"不能只加页面
很多人第一次做这个需求,第一反应是写一个 login.html,再写个接口校验用户名密码,就以为完了。实际上一旦做了登录拦截,你要处理的不只是"登录"这一个动作,而是一整条链路:
- 登录页本身必须放行,否则会循环重定向到登录页又回到登录页。
- 登录接口必须放行,否则表单提交直接被拦截器拦下。
- Swagger 的资源如果放在拦截范围内,未登录用户访问这些资源时会被重定向到登录页,导致登录成功后页面样式和脚本加载不完整。
- 登录成功后要跳转到 swagger-ui.html,但跳转后的二次资源加载如果仍然被拦,就要重新走一遍会话校验。
这还只是最基础的逻辑。实际项目里还要考虑多环境配置:开发环境不想每次都登录,测试环境和生产环境又必须强制登录。所以这个需求真正考验的是"路径梳理"和"方案选型",不是那几行登录代码。
1.2 三种主流方案的取舍
我自己的经验是,这种场景下有三种主流做法,各有各的适用场景。
第一种是 Spring MVC 的 HandlerInterceptor。它只拦截 controller 层的请求,代码量最小、概念最简单,普通开发者一眼能看懂,适合大多数 SpringBoot 单体项目。缺点是不会拦截静态资源的直接访问,但 Swagger 的入口都是 controller 映射的 URL,所以用它足够。
第二种是 Servlet Filter。Filter 比拦截器更底层,可以拦到静态资源、JSP、任何 Servlet 路径。它的好处是控制力强,坏处也是控制力太强——你不小心就可能把 JS、CSS、图片全部拦了,排查起来比拦截器麻烦。除非你有特殊需求(比如统一给所有请求加白名单校验),否则我建议优先用拦截器。
第三种是 Spring Security。它是标准的安全框架,登录、会话、CSRF、密码加密全套都有,适合对安全要求高的项目。但代价是学习成本高、配置繁琐,而且 Spring Security 和 Springfox / springdoc 之间存在版本兼容问题,后面我会专门讲到。
我的建议是:中小型项目、团队里没有专门安全开发人员的,直接用拦截器方案;项目本身已经引入 Spring Security 的,就顺着 Security 的体系做,不要混用,否则两套会话体系会互相打架。
1.3 先搞清楚 Swagger 页面的资源加载链路
这是整个需求中最容易忽略、也最容易出错的地方。/swagger-ui.html 看起来只是一个页面,但它背后是一条完整的资源加载链:
- 浏览器请求
/swagger-ui.html,这是 springfox 提供的一个转发入口。 - 页面加载后,会向
/swagger-resources发起请求,获取当前项目里配置了哪些 Swagger 分组。 - 拿到分组后,再向
/v2/api-docs或/v3/api-docs请求接口定义 JSON,里面包含了所有 controller 的路径、参数、返回结构。 - 同时页面还需要加载
/webjars/springfox-swagger-ui/**下的一堆 JS、CSS、字体文件,这些是 Swagger UI 的静态资源。
也就是说,如果你只拦 /swagger-ui.html 这一个路径,未登录用户虽然打不开页面,但直接访问 /v2/api-docs 或者 /swagger-resources 一样能拿到接口数据。所以在配置拦截范围时,入口页面、接口文档 API、Swagger 资源配置这几类路径都要一起处理。
这个链路还会直接影响你配置登录成功后的跳转逻辑。我见过有人登录成功后跳转到 /,结果进了项目首页而不是文档页,还要手动再点一次菜单,体验很差。正确做法是登录成功后重定向回 /swagger-ui.html,让用户一步到位。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前先排雷:SpringBoot 高版本与 Swagger 的兼容性问题
2.1 springfox 在 SpringBoot 2.6+ 上的经典报错
热词里有人提到"springboot 版本太高",这确实是 Swagger 项目最常见的问题之一。如果你用的是 springfox 3.0.0 搭配 SpringBoot 2.6 及以上版本,启动项目时大概率会遇到类似这样的报错:
code复制Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
或者:
code复制java.lang.NoSuchMethodError: org.springframework.util.PathMatcher.combine(Ljava/lang/String;Ljava/lang/String;)Lorg/springframework/util/PathMatcher;
这个坑的根源在于 SpringBoot 2.6 之后,Spring MVC 的默认路径匹配策略从 AntPathMatcher 换成了 PathPatternParser。而 springfox 3.0.0 内部仍然依赖 AntPathMatcher,两者一冲突就启动失败。
解决方案是在 application.yml 里加一行配置,强制 Spring MVC 使用老的匹配策略:
yaml复制spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
加了这一行,绝大多数 springfox 3.0 项目都能正常启动。但要注意,这只是"兼容"方案,不是"根治"方案。如果项目里同时用了其他依赖 PathPatternParser 的组件,可能还会有隐性冲突。如果你在新建项目,我的建议是用 springdoc-openapi 替代 springfox,它对高版本 SpringBoot 的支持更积极,UI 入口也从 /swagger-ui.html 变成了 /swagger-ui/index.html,风格更现代,相关问题也少得多。
2.2 版本差异带来的路径变化
这里顺便把版本差异理清楚,因为很多网上教程只说 /swagger-ui.html,但不同框架、不同版本的实际访问路径不一样:
| 框架版本 | 默认访问路径 | 说明 |
|---|---|---|
| springfox 2.x | /swagger-ui.html |
经典入口,网上教程最多 |
| springfox 3.0.0 | /swagger-ui.html |
仍然兼容,但加了 /swagger-ui/ 新路径 |
| springdoc-openapi 1.x | /swagger-ui.html |
保持兼容旧习惯 |
| springdoc-openapi 2.x | /swagger-ui/index.html |
新路径,老路径会重定向 |
所以当你按照网上的教程配置完,发现 /swagger-ui.html 打不开,先别怀疑代码,看一眼自己用的到底哪个依赖、哪个版本。这个排查动作能省你半天时间。
2.3 生产环境到底要不要开 Swagger,要分开讨论
另一个我在实操中强烈建议做的决策是:把 Swagger 的开关和生产环境做隔离。换句话说,你在本地和测试环境方便调试没问题,但生产环境的 jar 包最好能直接关闭 Swagger。实现方式有很多种,最简单的是利用 SpringBoot 的 Profile 机制,在 application-prod.yml 里配置:
yaml复制springfox:
documentation:
enabled: false
配合配置类:
java复制@Configuration
@EnableSwagger2
@Profile({"dev", "test"})
public class SwaggerConfig {
// 你的 Docket Bean 配置
}
这样生产环境压根不会加载 Swagger 相关配置,"加登录页"这件事在生产环境就退化为"直接不提供文档",安全级别反而更高。注意 @Profile 不仅控制配置类加载,也控制 Docket Bean 的注册,所以生产环境启动时整个 Swagger 链路都不会初始化,这才是真正的关闭。
3. 用拦截器实现登录校验:最轻量、最好改的方案
3.1 自定义登录页面,放在 static 目录就行
既然要给 swagger-ui.html 加登录页,那登录页本身放在哪里很关键。如果你没有引入 Thymeleaf 等模板引擎,直接把 login.html 放在 src/main/resources/static 目录下,SpringBoot 会自动把它映射为静态资源,访问路径就是 /login.html。这个方案最简单,不需要额外的视图解析配置。
登录页的写法可以很朴素,核心就是表单提交到登录接口:
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>API 文档中心登录</title>
<style>
body {
margin: 0;
padding: 0;
background: #f0f2f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: "Microsoft YaHei", sans-serif;
}
.login-box {
width: 380px;
background: #fff;
padding: 40px;
border-radius: 10px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
.login-box h2 {
text-align: center;
margin-bottom: 24px;
color: #333;
}
.login-box input {
width: 100%;
height: 40px;
margin-bottom: 16px;
padding: 0 12px;
border: 1px solid #d9d9d9;
border-radius: 6px;
box-sizing: border-box;
font-size: 14px;
}
.login-box button {
width: 100%;
height: 40px;
background: #1677ff;
border: none;
border-radius: 6px;
color: #fff;
font-size: 15px;
cursor: pointer;
}
.error-tip {
color: #ff4d4f;
font-size: 13px;
margin-bottom: 10px;
text-align: center;
}
</style>
</head>
<body>
<div class="login-box">
<h2>API 文档中心</h2>
<form action="/login" method="post">
<input type="text" name="username" placeholder="用户名" required autocomplete="username"/>
<input type="password" name="password" placeholder="密码" required autocomplete="current-password"/>
<button type="submit">登 录</button>
</form>
<script>
if (location.search.indexOf("error=1") !== -1) {
document.write('<div class="error-tip">用户名或密码错误</div>');
}
</script>
</div>
</body>
</html>
这个页面我刻意没有加复杂的前端逻辑。原因很简单:登录页是给内部人员用的,验一下密码、进文档,越简单越不容易出错。有些人非要在登录页上做动态背景、滑块验证,不是说不行,但你要评估维护成本。如果你确实想做得好看一点,用 canvas 画一个点线背景效果也不难,我后面会提一句,但那属于锦上添花。
3.2 登录接口与会话处理
登录接口的核心职责是校验密码、写入会话、跳转文档页。最朴素的方式是用 Session 保存登录状态:
java复制@RestController
public class LoginController {
private static final String SWAGGER_USER = "swaggerUser";
@PostMapping("/login")
public void login(HttpServletRequest request, HttpServletResponse response,
String username, String password) throws IOException {
if (checkPassword(username, password)) {
request.getSession().setAttribute(SWAGGER_USER, username);
response.sendRedirect("/swagger-ui.html");
} else {
response.sendRedirect("/login.html?error=1");
}
}
@GetMapping("/logout")
public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException {
request.getSession().invalidate();
response.sendRedirect("/login.html");
}
private boolean checkPassword(String username, String password) {
// 实际项目中建议从配置文件或数据库读取,并加密存储
return "admin".equals(username) && "admin123".equals(password);
}
}
这里我故意用的是明文比对,方便演示。落到真实项目里,密码不要硬编码在代码中,更不要用明文。我个人常用的做法是:用户名密码放在 application.yml 的配置项里,密码用 BCrypt 加密后存储,比对时调用 BCryptPasswordEncoder.matches() 方法。这样就算配置文件泄露,也不会直接暴露密码明文。
还有一个容易被忽略的机制:Session 默认超时时间是 30 分钟。对于文档中心的场景,这个时间其实偏长。如果你希望用户每天进来自动重新登录,可以设置更短的超时时间,比如 15 分钟。个人经验是文档中心没有必要保持太久的登录态,因为接口文档并不是高频操作,用户看一会儿就关掉了,长时间保持反而增加了被他人使用的风险。
另一个细节是登录成功后发 sendRedirect。如果你用了前后端分离,这个接口需要返回 JSON 让前端自己跳转;但 Swagger 文档场景基本都是后端渲染或纯静态页面,直接重定向最省事。两种方式没有好坏,关键是和你的登录页类型匹配。
3.3 核心拦截器的实现
拦截器是整个方案的灵魂。
java复制public class SwaggerAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("swaggerUser") != null) {
return true;
}
response.sendRedirect(request.getContextPath() + "/login.html");
return false;
}
}
逻辑非常简单:Session 里有登录标记就放行,否则重定向到登录页。但我有两点提醒。
第一,request.getSession(false) 里的 false 很关键。getSession(true) 在任何时候都会创建一个新 Session,那意味着未登录用户每次被拦截到登录页时,服务端都会产生一个无意义的 Session 对象。在高并发下,这会造成 Session 内存占用上升。用 false 只有在已有 Session 时才返回,不主动创建,对资源更友好。
第二,如果你只想拦截特定的几个 Swagger 路径,不要用 addPathPatterns("/**") 全局拦截再搞一堆排除。原则是"白名单最小化、拦截路径精确化",这样配置的人一眼能看懂,后面接手的同事也不会被一大堆 exclude 路径搞晕。
3.4 注册拦截器并精确配置放行清单
把拦截器注册到 Spring MVC,需要实现 WebMvcConfigurer:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SwaggerAuthInterceptor())
.addPathPatterns(
"/swagger-ui.html",
"/swagger-ui/**",
"/v2/api-docs/**",
"/v3/api-docs/**",
"/swagger-resources/**",
"/webjars/springfox-swagger-ui/**"
)
.excludePathPatterns(
"/login.html",
"/login",
"/error"
);
}
}
这套配置把前面提到的 Swagger 资源链全部纳入了拦截范围,同时放行了登录页和登录接口。这里有一个细节值得展开:拦截器拦截的是 Spring MVC 的 handler 处理链路,像 /webjars/** 这样的资源,在 SpringBoot 中默认由 ResourceHttpRequestHandler 处理,也会经过 preHandle 方法,所以 addPathPatterns 里带上它是有效的。
未登录用户直接访问 /swagger-ui.html,会被重定向到 /login.html。登录成功后再访问 /swagger-ui.html,Session 里已经有标记,正常放行。此时页面加载过程中的二级资源请求(webjars、swagger-resources、api-docs)因为 Session 存在也都能过,这是整个流程里最关键的一环——如果登录成功但资源路径没进拦截范围或放行配置不对,会出现页面框架出来但接口列表空白的诡异情况。
3.5 多环境控制:开发环境放行,生产环境必须登录
实际部署中,开发环境不想每次启动都登录一次,太烦了。可以用一个简单开关来控制拦截器是否生效:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${swagger.auth.enabled:true}")
private boolean authEnabled;
@Override
public void addInterceptors(InterceptorRegistry registry) {
if (!authEnabled) {
return;
}
registry.addInterceptor(new SwaggerAuthInterceptor())
.addPathPatterns(
"/swagger-ui.html",
"/swagger-ui/**",
"/v2/api-docs/**",
"/v3/api-docs/**",
"/swagger-resources/**",
"/webjars/springfox-swagger-ui/**"
)
.excludePathPatterns("/login.html", "/login", "/error");
}
}
然后在不同环境配置文件里控制开关:
yaml复制# application-dev.yml
swagger:
auth:
enabled: false
# application-prod.yml
swagger:
auth:
enabled: true
这个做法我强烈推荐。因为开发环境你每天都可能要启动项目看接口调试,如果每次都输入用户名密码,会非常影响开发效率。而且这样做之后,测试登录流程的环境就是和生产环境一致的,不会出现"开发环境没问题、生产环境就出问题"的尴尬。
4. 如果项目用了 Spring Security,该怎么做登录校验
4.1 Spring Security 方案与拦截器方案的选择
有些项目本来就集成了 Spring Security 做接口权限,这时候你再写一个拦截器去校验 Session,两套体系容易冲突。最典型的问题:Spring Security 默认会拦截所有请求并要求认证,你写的 /login 接口可能根本进不去,表单提交直接被 Security 的过滤器链吃掉了,用户名密码对的也登录不了。
所以一旦项目里已有 Spring Security,正确的做法是顺着 Security 的体系改造,而不是另起炉灶。
4.2 SpringBoot 2.7+ 时代的配置写法
网上大量教程还在用 extends WebSecurityConfigurerAdapter 的写法,但这个类在 Spring Security 5.7 之后被标记为废弃,SpringBoot 2.7+ 推荐直接声明 SecurityFilterChain Bean。新写法长这样:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login.html", "/login").permitAll()
.requestMatchers(
"/swagger-ui.html",
"/swagger-ui/**",
"/v2/api-docs/**",
"/v3/api-docs/**",
"/swagger-resources/**",
"/webjars/**"
).authenticated()
.anyRequest().permitAll()
)
.formLogin(form -> form
.loginPage("/login.html")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/swagger-ui.html")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login.html")
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin123")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
}
这里有好几个细节值得展开。
csrf().disable() 是我在内部文档场景下的妥协。CSRF 防护针对的是 Cookie 自动携带的跨站请求,Swagger 文档中心一般没有浏览器跨站攻击的需求,关闭后表单登录更简单。但如果你做的是用户系统、交易系统,千万不要照抄这个 disable。
.requestMatchers("/login.html", "/login").permitAll() 放行登录页和登录处理接口,注意 Security 表单登录默认的登录处理 URL 就是 /login,它会拦截这个路径自己处理,不需要你再写 LoginController。你把用户名密码通过 POST 提交到 /login,Security 自动帮你校验并跳转到 defaultSuccessUrl。
用户信息这里我用了 InMemoryUserDetailsManager,适合演示。真实项目中我更推荐把用户信息放到数据库表里,实现 UserDetailsService 接口从库里查询,并且用 BCryptPasswordEncoder 加密。很多项目为了省事直接用明文,一旦被脱库,密码直接暴露,这个风险不值得。
4.3 Security 方案下的常见坑
Security 方案最常见的坑有三个,我一个个说。
第一个是静态资源被拦截。如果你配置 .anyRequest().authenticated(),意味着登录页引用的 CSS、JS 也被 Security 拦截,登录页样式全会丢失。所以要么在 requestMatchers 里放行 /css/**、/js/**、/images/** 等静态资源,要么保证登录页是纯内联样式、不依赖外部文件。我上面的登录页示例就故意全写内联样式,就是这个原因。
第二个是登录后的重定向问题。Spring Security 默认登录成功会跳回登录前访问的 URL,这个行为是好的,但如果你同时配置了 defaultSuccessUrl("/swagger-ui.html"),注意 defaultSuccessUrl 在用户直接访问登录页时是生效的,但有 pre-auth URL 时 Security 会优先跳回原 URL。实际效果通常是你直接访问 /swagger-ui.html,被踢到登录页,登录成功后再跳回 /swagger-ui.html,这是我们想要的闭环。
第三个是内存用户不能动态管理。InMemoryUserDetailsManager 的用户是写死在代码里的,改密码要重新发版。如果团队里经常换人,建议做一个简单的用户表,配合 admin 管理接口维护账号,比每次改配置重启灵活得多。
5. 踩坑实录与排查技巧,这部分至少省你半天时间
5.1 登录成功后页面样式全丢,接口列表空白
这个问题几乎是必踩的。现象是登录成功,跳到 swagger-ui.html,页面上能看到标题、菜单,但样式、图标都是乱的,接口列表一片空白。
原因在于 Swagger UI 页面的 JS、CSS 都是通过 /webjars/springfox-swagger-ui/** 加载的,而这些路径没有放到拦截放行清单里,或者放在拦截清单里但 Session 校验没过。前者会导致浏览器请求资源时被重定向到 /login.html,最终返回 HTML 而不是 JS 文件,控制台会报 MIME 类型错误;后者通常发生在空白页面前一步——登录成功跳转到 swagger-ui.html,浏览器并发请求大量 webjars 资源,其中一部分请求在会话刚刚创建时因为时序问题没带上 Cookie。
排查方法是打开浏览器开发者工具,切到 Network 标签,刷新页面看红色请求。如果发现某个 JS 请求的响应是 text/html,基本上就是被拦截器或 Security 重定向了。解决办法就是我把 /webjars/** 加入放行或拦截后的会话确认逻辑。
我自己的习惯是:Swagger 入口页面严格拦截,但 /webjars/** 直接放行。因为 webjars 里的 JS/CSS 是公共资源,不含敏感数据,放行它并不会泄露接口信息,但能避免非常多会话时序坑。真正需要严格保护的是 api-docs 接口,那里才是全部接口数据的源头。
5.2 登录成功但访问 swagger-ui.html 仍然跳回登录页
这个问题通常是 Session 没存上,或者存了但读取的 key 对不上。比如 LoginController 里存的是 swaggerUser,拦截器里取的是 loginUser,这种低级错误排查起来最快,但也最容易犯。我建议把 Session 的 key 定义为常量,放在一个公共类里,登录接口和拦截器引用同一个常量,从源头杜绝拼写不一致。
另一种情况是顺手设置了 Session 的最大存活时间,但把它设成了负值或太小的值。比如 tomcat 会话超时时间默认 30 分钟,你如果配置了 server.servlet.session.timeout=1m,用户看完文档再点一下页面,Session 就过期了,又会跳回登录页。这种情况不是 bug,是配置和预期不符,确认一下配置就行。
5.3 拦截器生效了但 swagger-ui.html 直接 404
拦截器配置没问题、登录也正常,但访问 /swagger-ui.html 返回 404,这大概率是 Swagger 本身没启动成功。两个常见原因:
一个是 springfox 3.0 搭配 SpringBoot 2.6+ 的路径匹配问题,启动时已经报错但你没有注意,导致 Swagger 的 mapping 没注册。解决方法是前面说的在 yml 里加 spring.mvc.pathmatch.matching-strategy: ant_path_matcher。
另一个是项目里同时引用了 springfox 和 springdoc 两套依赖,两个框架抢占了 /swagger-ui.html 的映射。检查一下依赖树,把重复的依赖排除掉。
5.4 Session 并发登录与安全审计
如果你想让同一账号只能在一处登录,需要自己在登录时维护一个 用户名 -> SessionId 的映射,登录时把旧 Session 踢下线。这个功能在文档中心场景可能用不上,但如果你把它扩展到后台管理系统,就很实用了。实现思路是在登录接口里用 ConcurrentHashMap 存映射,拦截器里判断当前 SessionId 是否等于映射中的值,不一致就重定向到登录页并提示"账号已在其他设备登录"。
不管做不做单点登录,我强烈建议给登录动作加一条日志,记录用户名、登录 IP、登录时间。原因很简单,Swagger 暴露的是全部接口文档,一旦账号泄露,运维可以通过日志快速定位是谁在什么时间登录的,安全审计的时候能拿出数据。
5.5 一些增强登录页的小技巧
先回应一下热词里提到的"vue3 登录页面 点线动态的背景"。如果你只是给 Swagger 文档中心加一个登录页,没必要引入 Vue3,一个静态 HTML 完全够用。但如果你确实想让页面更精致,可以用 canvas 画一个点线网络背景,核心代码量不大,几十行 JS 就能实现,本质是在画布上生成随机点,把距离较近的点用线段连接起来,再用 requestAnimationFrame 做动态效果。这个美化看团队审美,不影响功能。
我更推荐把精力花在实际的登录风险控制上。最简单的两招:一是登录接口加失败次数限制,连续错误 5 次锁定该 IP 或账号 15 分钟;二是密码不要明文传输,生产环境至少做一层加密。这些比视觉美化更值钱。
6. 其他值得补充的安全细节
6.1 生产环境最安全的方案是直接关闭 Swagger
前面提到过用 @Profile 隔离 Swagger,这里我再补充一点:如果你对安全要求极高,甚至不想要"登录后访问文档"这个能力,那生产环境直接关闭是唯一正确的选择。原因在于,只要接口文档存在,就存在被爆破、被越权访问的风险,哪怕隔着登录页也一样。对于对外服务的生产系统,我个人的建议是:生产环境彻底关闭 Swagger,开发测试环境保留,这样攻击面最小。
6.2 密码配置与多账号管理的具体建议
用了拦截器方案做登录,密码建议放在配置中心或环境变量里,配合 Spring 的配置加密组件。多账号的话,我推荐实现一个简单的用户 Service,用数据库表维护账号。表结构不需要复杂,用户名、密码(BCrypt 加密)、角色、状态、最近登录时间,这几列就够了。登录校验时查库比对,状态为禁用的一律拒绝。数据库方式虽然多写几行代码,但以后删人、改密、审计都好办,别为了省这几行代码把密码写死在代码里,那是给自己挖坑。
6.3 把登录逻辑和业务代码解耦
最后说一个从架构层面看的建议。不管用拦截器还是 Security,登录校验逻辑都应该独立成一个模块,不要分散写在各个 Controller 里。这样以后公司要统一接入单点登录,你只需要替换这个模块的内部实现,业务代码一行不用改。我在实际项目中的体会是:安全相关的代码最怕的就是"各处来一段",因为很难收拢,排查问题时全凭记忆,代价很大。
这个需求做下来,你会发现核心工作量其实很集中:先把 Swagger 的资源链路搞清楚,再选一个合适的方案,然后精确配置拦截范围。剩下的就是重复的联调和踩坑。按照这篇文章的顺序,从方案选型开始,先本地跑通拦截器方案,再根据项目现状决定要不要升级到 Spring Security,最后在生产环境做好开关控制,整个流程会顺畅很多。
