
Spring Boot 3 博客项目接口文档与安全配置实操手记最近在整理一个 Spring Boot 3 的个人博客项目后端接口写完之后发现团队协作时有个很现实的问题接口文档全靠口头传达前端同学天天来问“这个字段啥意思”“这个接口要什么参数”“为什么我一调就 401”。说实话每个写后端的人都经历过这个阶段。后来我把 Swagger 3也就是 OpenAPI 3集成进去再把基本认证Basic Auth配上整个协作流程顺畅了很多。这篇文章不是泛泛而谈理论而是把一个真实项目从零开始配置 Spring Boot 3 Swagger 3 Basic Auth 的完整过程记录下来。里面包含了我实际踩过的坑、最后采用的方案以及每一个关键配置背后的思考。如果你正在做 Spring Boot 接口开发或者想把项目里的接口文档规范化这篇文章应该能帮上忙。1. Spring Boot 3 项目里怎么理解 Swagger 3 和 OpenAPI 31.1 Spring Boot 3 最大的变化是什么Spring Boot 3.0 在 2022 年底正式发布最核心的变化是 Java 17 成为基础、Jakarta EE 9/10 取代了原来的 Java EE 8也就是说原来javax.*包下的注解全部换成了jakarta.*。别小看这个变化它对整个生态的影响非常深远。大多数第三方库如果不跟着升级在 Spring Boot 3 下根本跑不起来。这一点对 Swagger 生态的影响格外明显。老牌的 springfox 项目Swagger 2 时代的主力长期停留在 3.0.0 版本没有适配 Jakarta EE 的更新社区基本已经放弃。所以在 Spring Boot 3 项目中springdoc-openapi 是事实上唯一靠谱的选择。项目里用的依赖是dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-starter-webmvc-ui/artifactId version2.6.0/version /dependency之所以需要 starter、webmvc、ui 这三个关键词一起是因为这个包同时包含了 OpenAPI 3 的自动配置和 Swagger UI 的前端资源。如果你的项目用的是 WebFlux 而不是 Spring MVC那需要换成springdoc-openapi-starter-webflux-ui。这两者的坐标长得非常像但千万别搞混不然启动时会直接报 ClassNotFoundException。1.2 Swagger 3 与 Swagger 2 的核心差异老实说很多文章把 Swagger 2 和 Swagger 3 混在一起讲导致新手特别容易混淆。这里先梳理清楚Swagger 2 时代的规范叫 OpenAPI 2.0它的文档描述文件结构是 JSON对象层级复杂而且没有标准化的注解机制通常还得依赖 springfox 这个第三方适配层。Swagger 3 时代对应的是 OpenAPI 3.0 规范它的文档描述用的是规范化的 YAML/JSON 结构引入了 components 区块来统一复用模型定义同时官方注解已经完全标准化。更直观的区别在注解上// Swagger 2 时代一个接口的写法 ApiOperation(value 获取文章列表, notes 分页获取) ApiImplicitParams({ ApiImplicitParam(name page, value 页码, required true, dataType int, paramType query) }) GetMapping(/articles) // OpenAPI 3 时代同一个接口的写法 Operation(summary 获取文章列表, description 分页获取) Parameter(name page, description 页码, required true) GetMapping(/articles)从项目维护和代码可读性的角度来说OpenAPI 3 的注解类名更直观Operation一眼就能看出是描述接口操作的。Tag表示所属分组Schema描述数据模型字段非常贴近 REST API 的语义。在项目中Model 层的注解也有明显变化。Swagger 2 用ApiModel和ApiModelProperty来描述实体类而现在只需要用Schema就能完成同样的功能。这个注解来自io.swagger.core.v3.annotations.media.Schema。2. 项目基础环境与实际配置一览2.1 技术栈与版本组合说明在整个配置过程中版本兼容性是我最在意的问题。Spring Boot 3 本身就要求所有依赖必须适配 Jakarta EE而 springdoc-openapi 2.x 是专门为 Spring Boot 3 设计的。项目实际使用的版本组合如下组件版本说明JDK17Spring Boot 3 的最低要求Spring Boot3.2.5当前稳定版本springdoc-openapi2.6.0适配 Spring Boot 3 的 Swagger 3 实现Spring Security6.2.4Spring Boot 3 默认集成的安全框架MyBatis-Plus3.5.6持久层框架这里特别提醒一下springdoc-openapi 的版本需要与 Spring Boot 3 的小版本尽量匹配。我当时第一次尝试用 2.2.0 搭配 Spring Boot 3.2.5启动时发现 Swagger UI 的资源路径 404后来升级到 2.6.0 才解决。其实不是功能上的 Bug而是旧版本对 Spring Boot 3.2 的路径映射适配不完整。2.2 核心依赖与配置文件配置的核心依赖在 pom.xml 中实际项目里的完整依赖块是这样的!-- OpenAPI 3 文档生成 Swagger UI -- dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-starter-webmvc-ui/artifactId version2.6.0/version /dependency !-- Spring Security 安全框架 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency !-- Web 支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependencySpring Security 这个依赖加上之后项目就会自动把所有的接口都保护起来。默认情况下Spring Boot 3 的 Security 会自动生成一个随机密码用户名是 user这个密码在项目启动时的控制台日志里可以看到。这个机制在开发阶段其实很烦人因为每次重启密码都会变。后面的章节里我会给出解决方案这里先有个概念。在 application.yml 配置文件里我做了三组核心配置应用基础信息、Springdoc 的文档路径定义、以及数据源配置。这里只展示与本文主题最相关的部分spring: application: name: blog-api springdoc: api-docs: path: /v3/api-docs swagger-ui: path: /swagger-ui.html operations-sorter: method tags-sorter: alpha display-request-duration: true show-actuator: true server: port: 8080 servlet: context-path: /blog这个配置最容易忽略的地方是server.servlet.context-path。如果你的项目设置了 context-pathSwagger UI 的实际访问路径就是http://localhost:8080/blog/swagger-ui.html很多人在这一步找不到文档页面其实是忘了带上 context-path。3. 核心配置文件逐行解析OpenAPI 配置类与企业级细节3.1 为什么要单独写一个 OpenAPI 配置类springdoc-openapi 虽然能自动扫描 Controller 生成文档但自动生成的文档默认信息很少。为了让文档具有项目识别度必须在配置类中集中定义 OpenAPI 的元数据信息。我在项目中建了一个OpenApiConfig配置类核心代码如下package com.blog.demo.config; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.security.SecurityRequirement; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class OpenApiConfig { Bean public OpenAPI blogOpenAPI() { return new OpenAPI() .info(new Info() .title(个人博客 API 文档) .description(基于 Spring Boot 3 的个人博客系统接口文档包含文章、分类、标签、评论等模块。) .version(v1.0.0) .contact(new Contact() .name(博客开发团队) .email(devexample.com) .url(https://example.com))) .components(new Components() .addSecuritySchemes(basicAuth, new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme(basic) .description(使用用户名和密码进行基本认证。格式Authorization: Basic base64(username:password)))) .addSecurityItem(new SecurityRequirement().addList(basicAuth)); } }这里SecurityScheme.Type.HTTP配合scheme(basic)是在 OpenAPI 规范中声明 Basic Auth 的标准方式。addSecurityItem表示文档中默认所有接口都要求携带认证信息UI 右上角会出现一个 Authorize 按钮。3.2 控制器注释实用写法从直接能用开始Controller 层才是大部分开发者比较陌生的地方。我在博客项目的每个接口上都加了注解实际代码是这么写的Tag(name 文章管理, description 文章增删改查接口) RestController RequestMapping(/api/articles) public class ArticleController { Operation(summary 分页获取文章列表, description 按创建时间倒序返回文章列表) Parameter(name page, description 页码从 0 开始, example 0) Parameter(name size, description 每页条数, example 10) GetMapping public PageResultArticleVO list(RequestParam(defaultValue 0) int page, RequestParam(defaultValue 10) int size) { return articleService.pageQuery(page, size); } Operation(summary 获取文章详情, description 根据文章 ID 获取完整详情包含正文内容) GetMapping(/{id}) public ArticleDetailVO detail(PathVariable Long id) { return articleService.getById(id); } Operation(summary 创建文章, description 需要登录状态标题和正文必填) PostMapping public ResultLong create(RequestBody Valid ArticleCreateRequest request) { return Result.ok(articleService.create(request)); } }这里需要提醒一个容易忽略的技术细节类级别用Tag方法级别用Operation属性用Parameter或者模型字段上的Schema。这个组合是 OpenAPI 3 的标准注释体系在 Swagger UI 渲染时Tag作为一级分组Operation作为每个接口的说明卡片Parameter对应每个请求参数。3.3 几个注解的实际效果演示实际操作之后文档页面的展示大致是这样的结构文章管理Tag 分组GET /api/articles分页获取文章列表参数pagequery默认 0、sizequery默认 10响应PageResultArticleVOGET /api/articles/{id}获取文章详情参数idpath必填POST /api/articles创建文章请求体ArticleCreateRequest如果再配合Schema给 DTO 字段加上注释和示例值文档的可读性会更好public class ArticleCreateRequest { Schema(description 文章标题, example Spring Boot 3 入门指南, requiredMode Schema.RequiredMode.REQUIRED) private String title; Schema(description 文章正文支持 Markdown 格式, example ## 一、环境准备...) private String content; }如果你看过别的项目里那些排版混乱、字段含义不明的接口文档就知道这些注释的作用有多大。以前前后端对接一个问题要来回确认好几次现在前端直接看文档就能自己对接了沟通时间大幅缩减。4. Basic Auth 自定义配置从禁用默认到完整实现4.1 为什么选择 Basic Auth 而不用 OAuth2很多人一听到“安全认证”就联想到 OAuth2、JWT但在个人博客项目这类场景下Basic Auth 反而是更务实的方案。原因很简单Basic Auth 的实现成本低只需要一个用户名、一个密码和标准的 HTTP 头不需要搭建独立的认证服务器也不需要考虑 Token 刷新和过期机制。开发阶段用 Basic Auth 有三层便利第一Swagger UI 支持“Authorize”按钮直接填入账号密码调试接口不需要额外写 Token 逻辑第二Postman 等工具天然支持 Basic Auth在 Authorization 标签下选择 Basic Auth 填账号密码即可第三后端实现非常直白用 Spring Security 的InMemoryUserDetailsManager或者数据库用户表配一个 DaoAuthenticationProvider 就行。需要注意的是Basic Auth 的凭据只是在 HTTP 头中附带 Base64 编码并没有加密。所以在生产环境部署时必须配合 HTTPS 使用否则凭据很容易被拦截。这是 Basic Auth 的核心限制。4.2 完整的安全配置类逐行说明项目的安全配置类是整个认证体系的核心代码如下package com.blog.demo.config; 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 SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth .requestMatchers( /v3/api-docs/**, /swagger-ui/**, /swagger-ui.html, /webjars/** ).permitAll() .requestMatchers(/api/public/**).permitAll() .anyRequest().authenticated() ) .httpBasic(basic - { basic.realmName(Blog API); }); return http.build(); } Bean public UserDetailsService userDetailsService() { UserDetails admin User.withUsername(admin) .password(passwordEncoder().encode(admin123)) .roles(ADMIN) .build(); UserDetails user User.withUsername(user) .password(passwordEncoder().encode(user123)) .roles(USER) .build(); return new InMemoryUserDetailsManager(admin, user); } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }逐行拆解一下csrf(csrf - csrf.disable())这行代码在无状态 API 项目中是标准的操作。CSRF 防护本来就是针对“基于 Cookie 的会话”设计的而 Basic Auth 的凭据放在 Authorization 头中不受跨站请求伪造的威胁影响所以可以安全关闭。如果你的项目中还有基于 Cookie 的 Session 登录那这行一定不能省。授权规则部分定义了头和尾两种信息。首先是 Swagger 的文档路径需要公开放行否则没登录就根本看不到文档页面。其次是项目的公开接口比如文章列表允许匿名访问其他接口全部要求认证。.httpBasic(basic - basic.realmName(Blog API))的 realmName 会在弹窗身份验证时显示提示信息但在浏览器直接访问时会以 401 响应头中 WWW-Authenticate 的 realm 形式出现。这个参数只在某些浏览器或客户端工具中会展示实际影响有限但写上也不算多余。4.3 数据库用户模式与内存模式的选择上面用的是 InMemoryUserDetailsManager用户信息硬编码在代码里适合开发演示。但生产环境的真实项目里用户通常存在数据库表中需要在 UserDetailsService 的实现类里把查库逻辑接进去。这里给一个常见的 DAO 实现示例Service public class BlogUserDetailsService implements UserDetailsService { private final UserMapper userMapper; Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserEntity entity userMapper.selectByUsername(username); if (entity null) { throw new UsernameNotFoundException(用户不存在); } return org.springframework.security.core.userdetails.User .withUsername(entity.getUsername()) .password(entity.getPassword()) .roles(entity.getRole()) .build(); } }这里有个很多新手容易犯的错数据库表里存的密码必须是 BCrypt 加密后的密文而不是明文。如果直接用明文Spring Security 在密码比对时会校验失败报错信息通常很隐晦需要排查很久才能发现。我在项目里其实用了数据库模式把用户表和角色表都建好了配置只是把userDetailsService这个 Bean 替换为 DaoAuthenticationProvider 的版本。不过为了让这篇文章能直接演示我保留了最简的内存模式版本作为说明。5. 前后端联调Swagger UI 与 Basic Auth 的实际联动方式5.1 Swagger UI 页面上如何填写认证信息集成好之后启动项目然后访问http://localhost:8080/blog/swagger-ui.html会看到 Swagger UI 加载出一个包含所有接口的文档页面。如果全局配置了addSecurityItem那么页面右上角会有一个Authorize按钮。点击后会弹出一个对话框需要输入 Username 和 Password。填完之后点击 AuthorizeSwagger UI 就会把用户名和密码用 Base64 编码后拼到Authorization: Basic xxx请求头中发送给后端。这里有一个实际操作中比较容易踩的坑如果你在 OpenAPI 配置类里声明了 Basic Auth 方案但在 Spring Security 配置中把某些接口设为 permitAll那么这些接口其实根本不需要认证。Swagger UI 的 Authorize 按钮主要是让后续的请求自动携带认证头并不会强制拦截。真正拦截逻辑的权限定义完全由 Spring Security 控制。5.2 全局安全声明与局部放行的匹配关系在验收时需要注意一个重要原则OpenAPI 配置类的addSecurityItem是让文档默认展示所有接口受保护但 Spring Security 的实际拦截规则是另一套逻辑。二者是独立的。举例来说我在 OpenAPI 配置中声明了全局需要 Basic Auth但在 SecurityConfig 中放行了/api/public/**。前端在 Swagger UI 上看到的文档依然会显示所有接口都有锁图标但在未认证状态下访问/api/public/**是可以成功的。这一点如果不理解很容易产生冲突的认知偏差。如果希望文档精度更严谨可以在单独的接口上加SecurityRequirements()表示该接口不需要认证。比如Operation(summary 公开文章列表) SecurityRequirements GetMapping(/api/public/articles) public ResultListArticleVO publicArticles() { return Result.ok(articleService.listAll()); }需要注意的是这只是文档层面的展示实际能不能访问还是由 Spring Security 的过滤器链决定。5.3 关键测试未认证、错误密码、正确密码三种情况一个完整的接口测试至少需要覆盖三种情况。这里我用 curl 命令做演示实际项目中也可以直接用 Postman 或浏览器开发者工具来验证。未认证请求curl -i http://localhost:8080/blog/api/articles返回的是 401 Unauthorized响应头中带着WWW-Authenticate: Basic realmBlog API这个消息头是浏览器弹登录框的触发信号。如果你用 Postman 调试时看到这个响应头说明 Basic Auth 的配置是生效的。使用错误密码curl -i -u admin:wrongpwd http://localhost:8080/blog/api/articles返回同样是 401这时候可以在服务端日志中看到 Spring Security 的认证失败记录。Spring Security 6 的日志默认级别是 INFO如果输错了密码日志里会打印类似Authentication failure的信息。不过不建议在生产环境把安全日志设为 DEBUG因为用户名比对失败的信息也会跟着泄露。正确认证请求curl -i -u admin:admin123 http://localhost:8080/blog/api/articles这一次应该返回 200 OK 加上接口返回的 JSON 数据。这三个步骤走下来Basic Auth 整套链路就算验证通了。如果任何一个环节的状态码不对下一节直接查那张排查表。6. 实际开发中最容易出问题的 6 个故障现象与解决办法6.1 文档页面 404区分路径与依赖问题故障现象访问 swagger-ui.html 时白屏浏览器 Console 报 404。依次排查以下可能性路径没带 context-path如果配置了server.servlet.context-path: /blog访问地址必须带上否则一定 404。依赖坐标错误webflux 项目用了 webmvc 的依赖反之亦然。启动时报 ClassNotFound 的就是这种。springdoc 版本过旧Spring Boot 3.2 用 springdoc 2.6.0 以上比较稳。6.2 认证信息通过了但接口依然返回 403这种现象在 Spring Security 6 里比较常见。核心原因是 CSRF 防护没有关闭而你在认证时是通过 Swagger UI 的 Authorize 按钮带上 Basic Auth 头但 POST/PUT/DELETE 请求中缺少 CSRF Token所以被拦截了。解决办法就是前面配置里的csrf(csrf - csrf.disable())。如果你认为这个接口可能被 SSR 页面引用那可以保留 CSRF 并配置CsrfTokenRepository不过对于纯 API 来说没有必要。6.3 Swagger UI 能打开但所有接口都是 401很可能是因为你访问的是通过/v3/api-docs预览接口列表页面但这个路径本身也被 Basic Auth 保护了。需要在 SecurityConfig 中把/swagger-ui/**、/v3/api-docs/**加入 permitAll。在开发阶段还可以用springdoc.swagger-ui.disable控制文档页面的启停。6.4 配置多个密码后用户始终以最后一个生效InMemoryUserDetailsManager是支持多用户的但要确保不要把它重命名成两个同名 Bean。如果你的「用户A」和「用户B」在代码中用相同的用户名注册了两次实际上后一个会覆盖前一个。为避免这类问题建议用不同的用户名或者在代码中维护一个明确的用户列表。6.5 密码加密方式不一致导致永远认证失败项目中数据库存的密码必须用 BCrypt或其他 Spring Security 支持的 PasswordEncoder加密。你可以用下面的代码生成密文然后插入数据库public class PasswordGenerator { public static void main(String[] args) { BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); String hash encoder.encode(admin123); System.out.println(hash); } }把生成后的 hash 值存入数据库的 password 字段。encode方法每次生成的 hash 值不同这是 BCrypt 的加盐机制导致的正常现象不影响验证。6.6 集成 Basic Auth 后 Swagger UI 页面加载缓慢Swagger UI 在初次加载时需要从多个 CDN 拉取 JS/CSS 资源。内网或者网络受限的情况下页面可能会一直转圈。这时需要在配置中把静态资源全部走本地化加载springdoc 默认使用 CDN 的方式给 UI 页面加载 js、css 资源。配置方式如下springdoc: swagger-ui: use-root-path: true disable-swagger-default-url: true cdn-enabled: false但 CDN 的 URL 目前是硬编码在 springdoc 的配置类中的个别版本不提供从外部配置的入口。如果公司环境严格限制外网这个选项确实会有副作用。我的处理方案是在接口文档只在内网服务器上开放网关层做 IP 白名单限制这样即便加载缓慢也只是内部网络延迟导致的问题。7. 项目还能怎么扩展从 OpenAPI 3 向下延伸的几个方向7.1 用 springdoc 的 JWT 模式切换如果以后项目从 Basic Auth 升级为更安全的 Token 方案比如 OAuth2 或 JWT可以在 OpenAPI 配置中把 SecurityScheme 的类型从 HTTPbasic 改成 HTTPbearernew SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme(bearer) .bearerFormat(JWT) .description(直接在 Authorize 弹窗中粘贴 JWT Token);前端只需把Authorization: Basic xxx替换成Authorization: Bearer xxx后端将 httpBasic 换成 JWT 过滤器。这个改造的平滑度是 Spring Security 设计得比较好的一点。7.2 分组配置让文档按模块分文件如果博客系统的接口按后台管理和前台展示拆分成两套服务可以用 GroupedOpenApi 来分组Bean public GroupedOpenApi adminApi() { return GroupedOpenApi.builder() .group(admin) .pathsToMatch(/api/admin/**) .build(); } Bean public GroupedOpenApi portalApi() { return GroupedOpenApi.builder() .group(portal) .pathsToMatch(/api/public/**) .build(); }页面右上角会有下拉框切换分组非常方便。而 Spring Security 这边对/api/admin/**可以独立授权给 ADMIN 角色分角色的权限控制就梳理清楚了。7.3 分环境开关开发环境开启、生产环境关闭为了防止生产环境接口被恶意扫描大部分项目都会在产线关闭文档页面。springdoc 的方案是配合.properties或环境变量来实现分环境开关# application-prod.yml springdoc: api-docs: enabled: false swagger-ui: enabled: false开发环境的 application-dev.yml 则正常开启。注意即使接口文档关闭/v3/api-docs也会一并失效因为两者由同一个开关控制。7.4 与 Mock 数据联动的技巧前端在做页面开发时往往等不到后端真实接口完成就能先走起来。接口文档定稿后OpenAPI 的 YAML/JSON 文件本身就是 Mock 服务的数据源。用一些主流的 API 开发工具比如 Apifox、Apipost直接导入 OpenAPI 描述文件能自动生成 Mock 接口端到端链路提前跑通。这些工具本质上都是把 OpenAPI 3 的 json 描述信息作为通用语言来用的所谓生态的威力就在这里。8. 总结与经验意见从 Spring Boot 3 Swagger 3 Basic Auth 这个组合的完整配置过程来看核心工作其实已经慢慢从“写接口”转移到了“设计接口文档”和“定义安全规则”上。Spring Boot 3 帮我们省去了繁琐的配置模板springdoc 用简单、标准的注解把文档表达的活从 Controller 里解脱出来Spring Security 则把认证链路收拢到一条清楚可靠的 HTTP 层通道上。我在实际项目里踩过的几个坑重述一遍供后来人参考第一Spring Boot 3 必须用 springdoc 而不是 springfox不要按 Swagger 2 时代的旧惯性去搜资料第二OpenAPI 规范里的安全声明和 Spring Security 的真实拦截是两套独立的机制展示归展示拦截归拦截二者都配置好才会得到期望的结果第三Basic Auth 虽然实现简单但生产环境必须配置 HTTPS 才能保证安全性。如果你正在做一个中小型项目需要快速搭建接口文档同时又不愿意引入过于复杂的认证体系那这套方案是最省心的选择。接下来可以按自己的需求扩展权限范围或者把数据源从内存用户切换到数据库用户。