简介:SpringBoot通用后台管理系统源码包,面向Java开发者和SpringBoot框架学习者,提供一个完整的后台管理项目示例。项目覆盖自动配置、Spring Data JPA/Hibernate数据持久化、MySQL数据库脚本、Spring Security权限控制、Thymeleaf页面模板以及Swagger接口文档等典型技术实践,同时涉及前后端分离、Docker容器化部署等主流应用思路,既可作为课堂教学配套案例,也能作为企业级后台快速开发的参考脚手架。压缩包共152个文件,大小约623KB,其中包含129个Java源码文件,以及xml映射配置、properties全局配置、gradle构建脚本、sql初始化脚本、jar依赖包、docx设计文档、png图片资源和README说明等;源码中可见用户管理、文件服务、图片处理等业务模块,结合文档可梳理模块划分与调用关系。当前已有109人学习,适合需要提升SpringBoot实战能力、深入理解后台系统分层架构的中级开发者。
1. 一个 Spring Boot 后台管理系统的活资源:从登录到权限控制,拿来就能改的业务底座
网上打包好的 Spring Boot 后台管理系统不少,但真正能让人在半天内跑通、一周内改完接入自己业务的并不多。这套通用后台管理系统压缩包,价值不在于代码量,而在于它把后台系统最绕不开的几件事串成了一条完整链路:用户如何登录、Token 如何签发与校验、接口如何按角色放行、菜单如何动态渲染、每一步操作怎么留痕。如果你正在给某个内部项目凑后台底座,或者刚开始接触企业级开发里的权限模型,这套系统的代码结构和配置方式,正好是能直接上手拆着学的参考对象。
2. 通用后台系统的骨架:技术选型、分层结构和应用配置
2.1 技术选型:Spring Boot + Spring Security + MyBatis-Plus + Redis
后台管理系统说白了就是一堆数据表的增删改查,外面套一个安全外壳。它的压力通常不高,并发有限,真正的复杂度在权限模型和接口规范上。这套系统的基础框架是 Spring Boot,认证授权走 Spring Security,数据访问用 MyBatis-Plus,MySQL 做主存储,Redis 用于缓存验证码和登录 Token。
选这套组合的理由很现实。Spring Security 虽然概念多、配置繁,但它对认证过滤器链和权限表达式的支持是 Spring 生态里最完整的;MyBatis-Plus 相比原生 MyBatis 省掉了大量单表 CRUD 的 XML 编写量,内置分页插件和字段自动填充,对后台管理系统这种典型场景非常顺手。Redis 在这里不是绝对必须,但把它放进去以后,验证码临时存储、登录状态主动失效都不需要动数据库,对多实例部署很友好。
值得强调的是,这套资源里的安全配置走的是 JWT 无状态方案,而不是传统 Session 方案。两者核心区别在服务端是否保存登录状态。JWT 方案把用户身份和过期时间编码进 Token,服务端只负责验签,天然适合前后端分离和多实例横向扩展;代价是 Token 一旦签发,短时间内无法主动撤销,所以系统里需要配合一个 Redis 黑名单或 Token 版本号来解决退出登录、改密码后的失效问题。这一点在后续章节会专门展开。
2.2 项目分层与目录结构:代码该放哪里,边界在哪
打开压缩包,映入眼帘的是标准 Maven 布局。单模块结构,按包名做功能切分,这是我比较推荐阅读顺序的资源类型,因为它没有多模块间的跳转负担,适合先建立整体印象。我在拆包时会先看整体目录,再看 security 包和 config 包,最后才看业务代码。
典型目录结构如下:
src/main/java/com/example/admin ├── common # 通用类:统一返回结果、全局异常处理、常量 ├── config # 配置类:Security、Redis、MyBatis-Plus、跨域 ├── controller # 接口层:接收请求、参数校验、返回统一结果 ├── service # 业务层:认证、用户、角色、菜单、日志 │ └── impl ├── mapper # 数据访问层:MyBatis-Plus 的 Mapper 接口 ├── entity # 数据库实体映射 ├── dto / vo # 入参对象与出参对象 ├── security # JWT 过滤器、Token 生成与校验、用户详情实现 ├── aspect # AOP 切面:操作日志、接口限流 └── utils # 工具类:加密、JWT、日期等 src/main/resources ├── mapper # 复杂 SQL 的 XML 文件 ├── sql # 数据库初始化脚本 ├── application.yml └── application-dev.yml / application-prod.yml分层的核心原则是单向依赖:controller 调用 service,service 调 mapper,entity 只在 mapper 和 service 层之间透传,dto 和 vo 负责与外层交互。单向依赖的价值在于,后续要换存储、换安全框架或者做单元测试,每一层都有明确的替换边界。我拿到资源时最先看 security 包,因为权限认证是理解整套系统的钥匙,只要看懂 JWT 过滤器链条,后续所有接口权限配置都能顺出来。
2.3 配置文件与应用启动参数:多环境拆分和数据库初始化
application.yml 是所有环境的公共配置,环境差异放到 application-dev.yml 和 application-prod.yml。启动时通过--spring.profiles.active指定环境,这也是标准做法。
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/admin_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: "改成你自己的密码" redis: host: localhost port: 6379 database: 0 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.example.admin.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl server: port: 8080 servlet: context-path: /api jwt: secret: "这里是一段至少32位的签名密钥,务必换成自己的" expiration: 86400000 header: Authorization prefix: "Bearer "这段配置里有几个关键点。第一个是 MySQL 连接串,useUnicode=true&characterEncoding=utf8这两个参数缺一不可,漏掉后中文写入会乱码;serverTimezone=Asia/Shanghai解决时区差八小时的问题。第二个是 MyBatis-Plus 的map-underscore-to-camel-case,开启后数据库下划线字段自动映射为驼峰属性,实体类里就不用写一堆 @TableField。第三个是jwt.secret,这套系统里所有 Token 都由它签名,长度建议至少 32 位,部署到生产环境必须换掉默认值,不然别人拿到源码就能仿造你的 Token。
数据库初始化涉及 SQL 脚本,资源包的sql目录下一般有init.sql和data.sql,前者建表,后者插入初始数据。首次导入时建议两个都执行,然后立刻改掉默认管理员密码。
3. 部署与验证:从 SQL 脚本到浏览器登录的完整路径
3.1 环境清单与版本匹配
在跑这套系统之前,先确认本机环境。JDK 建议 1.8 或 11,MySQL 用 5.7 或 8.0,Redis 3.2 以上,Maven 3.6 以上。前端如果用了 Vue,还需要 Node.js 14 以上。
版本不匹配是很常见的翻车点。比如 MySQL 8.0 的驱动类名变成了com.mysql.cj.jdbc.Driver,老项目里写的是com.mysql.jdbc.Driver,后者在新驱动下会直接启动失败。再比如 Spring Boot 2.6 之后,默认禁止了循环依赖,如果项目里有循环依赖的 Bean,启动时会报错“The dependencies of some of the beans in the application context form a cycle”,这种通常要去掉 @Autowired 改构造器注入。先把环境版本和 pom.xml 里依赖版本对齐,能省下大半个晚上的排查时间。
3.2 数据库初始化:SQL 脚本导入与初始账号定位
SQL 脚本在resources/sql目录下。按顺序执行,先建库,再导入表结构和基础数据。命令行方式如下:
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS admin_system DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p admin_system < sql/init.sql mysql -u root -p admin_system < sql/data.sql三个命令分别完成建库、建表、插初始数据。注意utf8mb4和utf8的差别,utf8mb4 能存完整的 Unicode 字符,包括 emoji 表情和一些特殊符号,后台系统只要涉及用户输入内容,都建议用 utf8mb4。
导入完成后看表结构,这类通用后台系统至少会有这几张核心表:用户表、角色表、菜单表、用户角色关联表、角色菜单关联表、操作日志表。初始账号一般写在 data.sql 里,常见的是admin / admin123,也有的是admin / 123456。登录后第一件事是去用户管理里改密码,这个习惯能避免很多安全问题。如果 data.sql 执行后登录不进去,先检查是不是 SQL 脚本里的密码字段是加密密文,和系统使用 BCrypt 还是 MD5 加密的算法不匹配。
3.3 后端启动验证与前端路由联动
数据库就绪后,用 Maven 打包启动后端:
mvn clean package -DskipTests java -jar target/admin-system-1.0.0.jar --spring.profiles.active=dev--spring.profiles.active=dev指加载 application-dev.yml 里的配置。启动日志中出现“Started Application in X seconds”表示成功。随后验证接口:
curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"admin123"}'登录接口会返回包含 Token 的 JSON。拿到 Token 以后,调用一个需要登录的接口测试:
curl -X GET http://localhost:8080/api/user/list \ -H "Authorization: Bearer <上一步拿到的token>"如果返回业务数据而不是 401,说明认证链路已经通了。很多这类系统内置了 Swagger 或 Knife4j 接口文档,启动后访问/swagger-ui.html或/doc.html就能看到所有接口清单和参数定义,这是理解系统功能最快的方式。
前端部分如果是独立 Vue 工程,则在web或frontend目录执行npm install然后npm run dev。注意前端代码里配置了代理转发,比如/api开头的请求会被转发到http://localhost:8080,这个代理配置如果没生效,前端页面会报请求跨域或者 404。优先排查 Vite 或 webpack 的 proxy 配置,而不是去后端改 CORS。
4. 核心实现解读:认证、授权与日志的落地细节
4.1 JWT 登录认证流程:Token 签发、校验与过滤器链
Spring Security 本身有一套复杂的过滤器链,JWT 方案的接入点是在 UsernamePasswordAuthenticationFilter 之前插入一个自定义过滤器,负责从请求头里解析 Token 并创建认证对象。核心逻辑大概是这样:
public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider tokenProvider; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String token = resolveToken(request); if (token != null && tokenProvider.validateToken(token)) { String username = tokenProvider.getUsernameFromToken(token); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } chain.doFilter(request, response); } private String resolveToken(HttpServletRequest request) { String bearer = request.getHeader("Authorization"); if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) { return bearer.substring(7); } return null; } }这段代码的要点在三个地方。OncePerRequestFilter保证每个请求只过滤一次,避免重复执行。resolveToken从请求头中取出 Token 并去掉 "Bearer " 前缀,前缀约定在配置文件的jwt.prefix里。最关键的是SecurityContextHolder.getContext().setAuthentication(...),这一步把认证对象放进安全上下文,后续的 @PreAuthorize 注解才能拿到用户角色信息做判断。
登录接口负责签发 Token,流程包括验证码校验、用户名密码校验、Token 生成和返回。密码校验这块,这类系统普遍用 BCrypt 加密存储,校验时passwordEncoder.matches(rawPassword, encodedPassword)返回 true 才放行。不用 MD5 的原因是 BCrypt 自带盐值,同样的明文每次加密结果不同,能有效对抗彩虹表攻击。
4.2 接口级权限控制:注解式鉴权与动态权限点
这套系统最实用的部分是权限控制。它采用 RBAC 模型,用户关联角色,角色关联菜单和权限点,接口层通过注解控制访问范围。比如用户管理接口:
@PreAuthorize("hasRole('ADMIN')") @GetMapping("/user/list") public Result pageUsers(@RequestParam Integer page, @RequestParam Integer size) { return Result.success(userService.page(page, size)); } @PreAuthorize("hasAuthority('system:user:delete')") @DeleteMapping("/user/{id}") public Result deleteUser(@PathVariable Long id) { userService.deleteById(id); return Result.success(); }hasRole('ADMIN')和hasAuthority('system:user:delete')的差别值得说清楚。hasRole 要求用户拥有角色,并且 Spring Security 会自动给角色名加上ROLE_前缀,数据库里的角色编码也要对应写ROLE_ADMIN;hasAuthority 则直接匹配权限点字符串,粒度更细。比如一个运营角色能查看用户列表但不能删除用户,就可以给它配置system:user:list权限而不配置system:user:delete权限。
实际项目里,权限点不是写死的,而是从数据库动态加载。用户登录时,系统把该用户所有角色对应的菜单权限点查出来,组装成一个权限集合放进认证对象;接口被访问时,注解里的权限字符串和这个集合做比对。我一般会重点看菜单表里perms字段的取值规则,它决定了前端按钮权限的动态渲染粒度。
4.3 操作日志的 AOP 实现:埋点、参数解析与异步落库
后台管理系统的审计需求一般靠操作日志解决。这套系统用 AOP 切面实现日志记录,不侵入业务代码,做法是在自定义注解上挂切面:
@Aspect @Component public class OperationLogAspect { @Around("@annotation(operationLog)") public Object record(ProceedingJoinPoint pjp, OperationLog operationLog) throws Throwable { long start = System.currentTimeMillis(); Object result = pjp.proceed(); long elapsed = System.currentTimeMillis() - start; // 从 SecurityContextHolder 获取当前操作人 // 从 RequestContextHolder 获取请求 IP 和访问路径 // 拼装日志对象,写入 sys_operation_log 表 return result; } }这个切面的设计思路是透明拦截。业务代码关注自己的逻辑,日志记录完全由切面完成。日志字段一般包括操作人、操作模块、操作类型、请求方法、请求路径、参数摘要、耗时和执行结果。参数摘要这里有个坑,不要把完整的密码参数打印进日志,这类资源一般会对敏感字段做脱敏处理,比如只保留账号前几位。
异步落库是这类日志模块的另一个重点。如果每记录一条日志都在请求线程里同步写库,等于给每个接口增加了额外延迟,高并发下会拖慢业务响应。常见做法是使用 Spring 的 @Async 注解把日志写入放到独立线程池里执行,或者先塞进 BlockingQueue 再定时批量落库。配置线程池时要关注队列容量和拒绝策略,队列满了以后是丢弃日志、调用方执行还是覆盖旧日志,决定了系统在极端情况下的行为。
5. 避坑实录:从环境到权限链路上最容易翻车的四个检查点
5.1 检查点一:数据库连接串少了参数,中文全变问号
之前一次部署中,厂商把数据库连接串简写成了jdbc:mysql://localhost:3306/admin_system,结果系统跑起来后所有录入的中文都变成问号。排查了半天,发现字符集连接的参数characterEncoding=utf8没带。数据库本身是 utf8mb4,但客户端连接时指定的字符集不对,写入自然就是乱码。
解决方式是把连接串补全:jdbc:mysql://localhost:3306/admin_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai。这里的useSSL=false也很重要,MySQL 8.0 默认开启 SSL 校验,本地环境没有配置证书时会报连接超时或者 SSL 握手失败。从这个坑之后,每次初始化环境我都先跑一条插入中文数据的 SQL,确认字符集没问题再继续。
5.2 检查点二:JWT 过滤器抛异常导致接口全部 401
一次联调中,前端反馈所有需要 Token 的接口突然全部返回 401,但登录接口本身是正常的。打开后端日志,发现错误堆栈是“JWT signature does not match”。排查后发现是运维在服务器上改了application-prod.yml里的jwt.secret,而前端本地调用的后端实例加载的还是旧配置文件,两个实例签出来的 Token 互相不能验证。
解决方式是统一配置管理,把 JWT 密钥集中放到配置中心或环境变量里,保证所有实例读到的是同一个值。另外要注意,改动jwt.secret后,所有在线用户手里的旧 Token 都会失效,这是预期行为,但要在变更通知里说清楚,让用户重新登录而不是当作故障处理。从那以后,密钥变更这类操作每次都要走配置变更评审流程。
5.3 检查点三:权限注解不生效,接口被匿名访问
在某个新写的控制器接口上加了@PreAuthorize("hasRole('ADMIN')"),但测试时发现普通用户也能正常访问。原因在于 @EnableGlobalMethodSecurity 注解没有打开。Spring Security 默认不处理方法级安全注解,必须在配置类上显式声明:
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig { // 相关配置... }prePostEnabled = true表示启用 @PreAuthorize 和 @PostAuthorize 系列注解。另一个原因是如果配置类上同时启用了 Spring AOP 和 CGLIB 代理,而注解方法所在的 Bean 创建了代理,权限判断被代理类拦截,但某些情况下代理没有生效。尤其是当你在同一个类内部调用带注解的方法,比如this.someMethod(),注解是不会被处理的,因为内部调用绕过了代理对象。
之所以说这个坑值得记录,是因为它的现象很隐蔽:系统一切正常,只是某个接口的权限控制悄悄失效了。排查时先看配置类有没有开启注解鉴权,再看调用链是不是经过代理对象。
5.4 检查点四:MyBatis-Plus 分页失效,查询返回全部数据
分页是后台系统的刚需,但 MyBatis-Plus 的分页需要注册分页插件,否则Page对象传进去,查询结果仍然是全表数据。常见场景是资源包里自带了一个MybatisPlusConfig配置类,但开发者把@Configuration注解误删了,或者类没有被 Spring 扫描到。
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }没有这个 Bean,selectPage实际上执行的是全量查询,然后把内存中的一部分返回给你,数据量大了以后内存和耗时都会出问题。排查方法是打开 MyBatis 的 SQL 日志,看执行语句里有没有LIMIT关键字;如果连 SQL 条件都带了但没 LIMIT,基本可以确定分页插件没注册。
6. 二次开发技巧:把它改成自己的业务底座,防住四个常见扩展问题
拿到资源之后,最关键的并不是看懂代码,而是知道自己要改哪些地方才能把它变成自己的系统。我按自己的经验总结一个固定流程,每接入一个新业务模块都走一遍。
第一个是加数据库表和对应实体。直接在 SQL 脚本里追加建表语句,然后在entity包下建实体类,mapper包下建 Mapper 接口。注意实体继承 BaseEntity 或按资源包既有的公共字段规则来写,这类系统通常会在 BaseEntity 里定义创建人、创建时间、更新人、更新时间和逻辑删除标记,这些字段由 MyBatis-Plus 自动填充,不需要在业务代码里手动赋值。加 Mapper 接口时,单表查询直接在接口里定义方法,加对应 XML 只处理多表关联查询。
第二个是加菜单和权限点。新增菜单要到菜单表里插数据,菜单编码、父级编码、路由路径、排序字段都不能空。权限点要和接口的@PreAuthorize("hasAuthority('xxx')")保持一致,否则前端配置了按钮权限后后端不认,或者后端要求权限但菜单里没配,接口会直接 403。菜单表的数据结构通常分为目录、菜单、按钮三级,按钮权限挂在菜单级别之下的perms字段里。
第三个是前端动态菜单的联动。这类系统的菜单大多是动态渲染的,前端根据登录接口返回的菜单树生成侧边栏。如果你新增了菜单但前端没有路由与之对应,点击菜单会跳转到空白页或 404。解决方法有两种:一种是后端返回的菜单 JSON 里包含前端路由的path和component字段,前端用map动态注册路由;另一种是在前端路由表里预先写好所有业务路由,后端只决定菜单的显示和隐藏。两种方案各有取舍,动态注册路由灵活但排错困难,预注册路由代码量多但直观。我倾向于后者,除非业务模块化程度非常高。
第四个是接口返回值格式的统一。这套系统的所有接口都返回Result结构,约定code = 200表示成功,其他表示业务失败,HTTP 状态码只反映请求是否被处理。在这个约定下,前端拦截器就可以统一处理错误提示和登录失效。千万不要在一个新加的接口里直接返回一个 Map 或者裸对象,这会让前端联调时多写一堆特殊判断。
我在一次交付中吃过这个亏,新增了一个导出接口,返回的是二进制流,没有走 Result 结构,前端鉴权拦截器拿不到 code 就不知道登录是否过期,结果导出临时失效时出现了一个很隐蔽的“弹窗不提示、接口静默失败”的问题。从那以后,我每次新增接口都强制要求走统一的返回封装,导出类接口也必须附一个错误时的 JSON 响应,把这个约定写进了团队的接口规范里。
如果这套通用后台系统是你第一次接触 JWT 认证,建议你先别急着改业务,把登录接口完整跑一遍,再看 JwtAuthenticationFilter 里的每个方法打印了什么日志,最后再动手加业务模块。理解认证链路对后续权限排查的帮助,远比你多写一个 CRUD 接口的价值大。希望这套系统能帮你省下搭底座的时间,把精力留在真正有业务逻辑的地方。
本文还有配套的精品资源,点击获取