简介:基于SpringBoot的通用后台管理系统,是一份面向Java后端初中级开发者及毕业设计人群的实战代码包。项目融合了数据持久层操作、权限控制、模板渲染等常见后台功能模块,适合作为管理系统快速开发脚手架或学习参考。压缩包共152个文件,以129个Java源文件为主,辅以XML配置、properties配置、SQL脚本、Gradle构建文件以及markdown和docx设计说明,整体大小约623KB,结构清晰便于按模块阅读。已有109人学习浏览,代码量不大但覆盖了从实体映射、业务服务到工具类的完整分层,如FileInfServiceImpl、UserServiceImpl、SmallPictUtil等类可在实际功能中直接对照。通过阅读构建脚本、数据库脚本和文件系统设计文档,可以快速理解项目启动流程与设计思路,适合用来巩固SpringBoot整合持久层框架与权限控制等知识,并在此基础上扩展为企业级后台系统。
1. springboot 通用后台管理系统:开箱即用的 RBAC 权限底座
接手外包项目,最怕从零搭后台权限底座:用户、角色、菜单三张表的关系能磨掉两天,登录认证、接口鉴权、操作日志又是一串重复劳动。这套 springboot 通用后台管理系统,就是直接开箱的权限底座——内置完整的 RBAC 权限模型、JWT 无状态登录、Shiro 接口鉴权和一套现成的管理界面。接外包或做内部系统的开发者能拿它快速交付,刚学 Spring Boot 的也能把它当研究权限落地的完整骨架。它解决的是后台系统里最重复的那部分:用户管理、角色分配、菜单与按钮权限、操作日志,剩下业务模块用自带代码生成器补齐。下面从架构、核心代码到启动步骤完整拆一遍。
2. 架构设计与技术选型:模块划分、RBAC 表结构与代码地图
2.1 技术栈组合:为什么这个搭配能撑起通用后台
先交代这套系统的技术构成。后端主体是 Spring Boot 2.x,ORM 用 MyBatis-Plus,认证授权用 Apache Shiro 加 JWT 的组合,缓存走 Redis,数据库是 MySQL,接口文档用 Knife4j 增强版 Swagger。前端是独立的管理模板工程,通过 HTTP 调用后端接口,菜单由接口动态下发渲染。
这个组合在中小项目里的成熟度很高,各组件分工明确:MyBatis-Plus 接管单表的增删改查和分页查询,业务表不用写重复的 XML;Shiro 负责认证与授权的抽象,JWT 把登录状态变成无状态 token,天然适合前后端分离部署;Redis 承接验证码、token 黑名单和热点数据。各层之间耦合度低,替换其中任何一个组件都不会牵动业务代码。
我自己的选择逻辑是:企业内网项目并发不高,用 Shiro 比 Spring Security 更省心,配置直观、学习曲线平缓,对刚上手的人友好。Spring Security 的过滤器链概念强大但抽象度高,排查权限问题时容易把新手挡在门外。这套系统选 Shiro 更多是出于上手门槛的考虑。功能模块上,系统管理通常覆盖用户管理、角色管理、菜单管理、操作日志、文件上传五块,业务模块则预留一两个示例 CRUD,等开发者用代码生成器去填充。
2.2 数据库设计:五张核心表组成的 RBAC 权限模型
权限系统最核心的是一套标准 RBAC(基于角色的访问控制)模型。这套系统的权限相关表共五张:用户表 sys_user、角色表 sys_role、菜单表 sys_menu,以及两张关联表 sys_user_role、sys_role_menu。下面是核心建表语句。
CREATE TABLE `sys_user` ( `user_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` VARCHAR(50) NOT NULL COMMENT '登录账号', `password` VARCHAR(100) NOT NULL COMMENT 'BCrypt加密后的密码', `nickname` VARCHAR(50) DEFAULT NULL COMMENT '显示昵称', `status` CHAR(1) DEFAULT '0' COMMENT '状态:0正常 1停用', `create_time` DATETIME DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`user_id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; CREATE TABLE `sys_role` ( `role_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '角色ID', `role_name` VARCHAR(50) NOT NULL COMMENT '角色名称', `role_key` VARCHAR(50) NOT NULL COMMENT '角色权限标识,如 admin', `status` CHAR(1) DEFAULT '0' COMMENT '状态:0正常 1停用', PRIMARY KEY (`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; CREATE TABLE `sys_menu` ( `menu_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '菜单ID', `parent_id` BIGINT DEFAULT '0' COMMENT '父菜单ID,0表示根', `menu_name` VARCHAR(50) NOT NULL COMMENT '菜单名称', `path` VARCHAR(200) DEFAULT NULL COMMENT '前端路由地址', `perms` VARCHAR(100) DEFAULT NULL COMMENT '按钮权限标识,如 system:user:add', `menu_type` CHAR(1) DEFAULT 'M' COMMENT '类型:M目录 C菜单 F按钮', `sort` INT DEFAULT '0' COMMENT '排序号', PRIMARY KEY (`menu_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单权限表';sys_user_role 与 sys_role_menu 是两张只含外键字段的关联表,分别记录用户拥有哪些角色、角色拥有哪些菜单。查询时靠这两张表把用户、角色、菜单串起来。
逻辑说明:用户不直接绑定权限,而是通过角色间接获得权限,新增用户只需分配已有角色,不必逐个勾选菜单。menu 表的 perms 字段是按钮级权限的关键,前端按钮是否显示、后端接口是否放行,最终都落到这个字符串标识上。menu_type 区分目录、菜单、按钮三层结构,前端动态渲染菜单时靠它决定路由层级。
参数说明:status 字段统一用 0 表示正常、1 表示停用,是后台系统约定俗成的状态位写法;perms 命名习惯是「模块:子模块:动作」,例如 system:user:add 对应新增用户按钮,保持这个命名规范,权限标识才能一眼看懂。表与表之间用逻辑外键而非物理外键,删除角色后不会因为外键约束把关联表卡死,这也是这类系统的常见取舍。
2.3 目录结构与功能模块清单:拿到源码先认准这七个包
后端工程标准结构如下,包名以 com.example.admin 为例:
src/main/java/com/example/admin ├── AdminApplication.java # 启动类 ├── common/ # 统一返回体 Result、全局异常、常量与分页对象 ├── config/ # Shiro、Redis、MyBatis-Plus、Swagger 配置类 ├── controller/ # 接口层,按模块分包:system、biz、auth ├── service/ # 业务逻辑层,接口+实现类 ├── mapper/ # MyBatis-Plus 的 Mapper 接口 ├── entity/ # 与数据库表对应的实体类 ├── shiro/ # JWT Filter、自定义 Realm、Token 封装 └── utils/ # JWT 工具、密码加密工具、文件工具 src/main/resources ├── application.yml # 数据源、Redis、端口等全部配置 ├── mapper/ # 自定义 SQL 的 XML 文件 └── sql/ # 建库建表脚本与初始化数据典型的四层分包:controller 只做参数接收和结果返回,service 放业务规则,mapper 只跟数据库打交道,entity 是纯数据结构。好处是定位代码快,新同事接手不用翻完整工程。resources 下的 sql 目录是关键,首次部署不用手动建表,直接导入脚本,默认管理员账号也在这里初始化。
拿到源码第一天,我建议先读 sql 目录和 application.yml,确认数据结构和配置项,再顺着 controller 往 service、mapper 逐层追,比从业务代码倒推要快得多。前端工程的结构一般对应后端的菜单树,路由表里每个组件路径和后端 menu 表的 path 字段一一对应,改菜单不要只改数据库,要前后端一起核对。
3. 核心模块拆解:登录、鉴权与日志的完整实现链路
3.1 登录认证:密码校验、JWT 签发与 Redis 缓存
登录接口是理解认证逻辑的入口。前端提交用户名和密码,后端做三步:查库校验密码、签发 JWT、把 token 写入 Redis。核心代码如下。
@RestController @RequestMapping("/api/auth") public class LoginController { @Resource private SysUserService userService; @Resource private JwtUtil jwtUtil; @Resource private RedisTemplate<String, String> redisTemplate; @PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { // 1. 校验用户名密码,数据库存的是 BCrypt 密文,不能明文比对 SysUser user = userService.checkPassword(dto.getUsername(), dto.getPassword()); if (user == null) { return Result.error("账号或密码错误"); } // 2. 签发 JWT,把用户 ID 和用户名写进 claims String token = jwtUtil.createToken(user.getUserId(), user.getUsername()); // 3. token 写 Redis,登出时能主动拉黑 redisTemplate.opsForValue().set("login:token:" + user.getUserId(), token, 2, TimeUnit.HOURS); return Result.ok(Collections.singletonMap("token", token)); } }逻辑说明:第 1 步的 checkPassword 内部用 BCrypt 的 matches 方法比对明文与密文,数据库只存密文;第 2 步签发的 JWT 带过期时间,默认两小时;第 3 步把 token 写 Redis 有两个目的,一是用户异地重新登录后旧 token 可以主动失效,二是登出接口直接删除 key 完成拉黑。
参数说明:LoginDTO 只有 username 和 password 两个字段,前端提交格式必须一致;token 过期时间在 JwtUtil 里配置,单位毫秒,2 * 60 * 60 * 1000 是两小时。开发调试期可以把过期时间调到 24 小时,省得频繁重新登录,上线前再改回来。这里有一个常见误用:把密码放进 JWT 的 claims 里,完全没必要,token 只要携带用户 ID 和用户名就够了,密码信息暴露在 token 里反而是风险。
3.2 接口鉴权:Shiro 过滤器链与 JWT Filter 的执行顺序
登录认证解决「你是谁」,接口鉴权解决「你能干什么」。这套系统的实现是全局过滤器链上挂一个自定义 JWT Filter,所有请求先过它:Filter 从请求头取出 token,解析出用户信息后交给 Shiro 完成登录状态绑定;之后由 Shiro 的授权机制判断当前用户是否拥有某个权限标识。
@Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager); Map<String, String> filterChain = new LinkedHashMap<>(); // 登录、验证码、接口文档不需要认证,直接放行 filterChain.put("/api/auth/login", "anon"); filterChain.put("/api/auth/captcha", "anon"); filterChain.put("/doc.html", "anon"); // 其余所有接口都走 jwt 过滤器 filterChain.put("/**", "jwt"); bean.setFilterChainDefinitionMap(filterChain); return bean; } }过滤链严格按照 Map 的插入顺序匹配,先匹配到的规则生效,所以放行接口必须写在 /** 之前,否则会被通配符吃掉,变成所有接口都要认证。jwt 过滤器解析成功后,会调用 SecurityUtils.getSubject().login(token) 把用户身份绑定到当前线程,后续的 @RequiresPermissions("system:user:add") 注解才能拿到授权信息,判断放行还是抛权限不足异常。
逻辑说明:这里有个前后端分离必须处理的差异——jwt 过滤器校验失败时要返回 JSON 格式的 401 响应,不能跳转登录页面。如果返回的是 HTML,前端会拿到一堆标签而不是可解析的错误对象,接口调用方的错误提示就会变成「解析失败」这种玄学报错。另外,自定义 Realm 里要重写 doGetAuthorizationInfo 方法,从数据库查出当前用户的角色和权限标识集合,Shiro 的注解判断就靠这份数据。
3.3 操作日志与全局异常:让每个请求都有迹可循
后台系统还有个刚需是操作日志。实现方式是自定义注解 @Log 加上 Spring AOP 切面,在 controller 方法上标注 @Log("新增用户"),切面在方法执行后把操作人、操作内容、请求参数、耗时写入日志表。切面核心代码如下。
@Aspect @Component public class LogAspect { @Resource private SysLogMapper logMapper; @AfterReturning("@annotation(controllerLog)") public void afterReturning(JoinPoint point, Log controllerLog) { SysLog log = new SysLog(); log.setTitle(controllerLog.value()); log.setMethod(point.getSignature().getName()); // 从 Shiro 会话里取当前登录用户 log.setOperName(SecurityUtils.getUsername()); log.setOperTime(new Date()); log.setRequestParams(Arrays.toString(point.getArgs())); logMapper.insert(log); } }逻辑说明:@AfterReturning 表示方法正常返回后才记录日志,业务异常不会被记成「操作成功」。操作人取自 Shiro 会话,前端不用传,也传不可信。请求参数直接序列化进日志表,排查线上问题很有用,但要注意日志里绝不能出现密码字段,写库前先做脱敏处理。
与切面配合的是全局异常处理器 @RestControllerAdvice,统一把业务异常、权限不足异常、参数校验异常转成 Result 格式的 JSON 返回,前端只需处理一种响应结构。service 层抛出自定义 BizException,全局处理器接住,message 直接展示给用户,省掉每层重复写 try-catch 的样板代码。这套「注解 + 切面 + 全局异常」的组合,几乎是所有通用后台的标配,理解透了对以后写别的项目也有直接帮助。
4. 本地跑起来:从环境准备到前后端联调的完整步骤
4.1 环境准备:JDK、MySQL、Redis 缺一不可
动手前先备齐依赖软件,常见版本要求如下表:
| 软件 | 建议版本 | 用途 |
|---|---|---|
| JDK | 1.8 或 11 | 运行 Spring Boot |
| Maven | 3.6 及以上 | 依赖管理与构建 |
| MySQL | 5.7 或 8.0 | 主数据库 |
| Redis | 5.x 及以上 | 验证码与 token 缓存 |
| Node.js | 14 及以上 | 前端工程构建(可选) |
判断标准很简单:MySQL 和 Redis 是硬依赖,不启动后端根本起不来;Node.js 只在你要跑前端工程时才需要。如果只想先验证后端接口逻辑,直接打开接口文档调试即可,前端可以延后再搭。
4.2 三步启动后端:导入 SQL、改配置、跑应用
第一步,导入数据库脚本。在终端执行:
mysql -uroot -p < sql/admin_db.sql脚本会建库建表并写入默认管理员账号。如果脚本里没有 create database 语句,先手动创建库,再指定库名导入。
第二步,修改 application.yml 的数据源与 Redis 配置。这一步的翻车率最高,拿到的源码大概率带着别人的开发配置,必须改成你本机的账号密码。
spring: datasource: url: jdbc:mysql://localhost:3306/admin_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 改成你自己的密码 redis: host: localhost port: 6379 server: port: 8080url 里的 serverTimezone=Asia/Shanghai 不能省,缺了它日期字段会差 8 小时,避坑章节还会专门说。Redis 如果有密码,在 redis 节点下加 password 字段;没有就留空。端口如果被占用,改 server.port 即可,注意前端代理地址也要同步改。
第三步,在工程根目录执行:
mvn spring-boot:run看到 Tomcat started on port(s): 8080 的日志,后端就算起来了。接着打开 http://localhost:8080/doc.html,登录接口试一次,能拿到 token 说明数据源、密码加密、JWT 签发这一整条链路都通了。
4.3 联调验证清单:登录、授权、登出全链路自测
后端跑通只是第一步,还要按清单过一遍,确认权限链路没有隐患:
- 用默认管理员账号登录,确认返回 token,且 Redis 里出现 login:token:* 的 key。
- 在接口文档里调用户列表接口,带 token 能返回数据,不带 token 返回 401。
- 用一个没有授权的新账号调删除用户的接口,确认返回 403 而不是 200。
- 调登出接口,旧 token 立即失效,再调业务接口返回 401,说明拉黑生效。
- 新建一个测试角色,只分配部分菜单,用该角色登录,确认前端只显示被授权的菜单。
提示:这套清单每次改动权限相关代码后都值得强制走一遍,五分钟能兜住绝大多数安全层面的低级错误。
前端工程要跑的话,解压前端目录后按常规流程 npm install、npm run dev,本地开发环境把接口地址代理到 http://localhost:8080 即可。前后端联调最常见的报错就是跨域,这类系统后端一般已经加了跨域过滤器,如果还是报跨域,优先检查过滤器顺序和 allowedOrigins 配置。
5. 避坑记录:启动失败、401 与菜单空白的高频排查
这一章的内容来自实际调试这类系统的踩坑记录,每条按「现象 → 原因 → 解决」写,遇到类似问题可以直接按顺序排查,能省下不少对着报错发呆的时间。
5.1 启动即失败:Failed to configure a DataSource 报错
现象:执行 mvn spring-boot:run 后控制台抛出 Failed to configure a DataSource: 'url' attribute is not specified,应用直接退出。
原因:application.yml 的数据源配置没有生效。常见两种情况:一是配置里 url 还指向别人的数据库地址和账号;二是 IDE 没把 src/main/resources 标记为资源根目录,配置文件没打进 classpath,Spring Boot 启动时根本读不到。
解决:先确认 resources 目录在 IDE 里显示为 Resources 根目录,再核对 yml 中 url、username、password 三项是否全改成本机环境。还有一个玄学点:yml 不能混用 Tab 和空格缩进,否则配置被静默忽略,报错还指向不明,排查起来很费时间。
5.2 接口全部 401:登录成功却进不了业务接口
现象:登录接口正常返回 token,但拿着 token 调业务接口,全部返回 401 未授权。
原因:前后端对 token 的传递位置不一致。前端把 token 放请求头 Authorization,而后端 JWT 过滤器读的是自定义头 token;或者在 Redis 里 token 已被新登录覆盖,旧 token 校验不过。
解决:统一成 Authorization: Bearer 的规范写法,后端过滤器先读 Authorization 头,再兼容旧的自定义头。排查时先看 Redis 里 login:token:* 是否真实存在,再比对过期时间,两步能定位大部分问题。要注意的是 JWT 本身是无状态的,服务端重启后签名密钥不变,旧 token 依然有效,如果希望重启即失效,必须依赖 Redis 里的校验记录,这也是这套设计把 token 同步写进 Redis 的根本原因。
5.3 菜单树空白:登录后侧边栏没有任何菜单
现象:登录成功进入首页,但左侧菜单一直空白或转圈,控制台要么没有请求,要么接口返回空数组。
原因:数据没配全。全新导入的数据库里,管理员账号可能没绑定角色,或角色没绑定菜单,接口返回的菜单列表是空数组。前端动态路由的 component 字段与真实组件路径不一致也会出现空白,这种情况浏览器控制台会提示找不到组件模块。
解决:先用 SQL 验证关联数据是否完整:
SELECT m.* FROM sys_menu m JOIN sys_role_menu rm ON m.menu_id = rm.menu_id JOIN sys_user_role ur ON rm.role_id = ur.role_id WHERE ur.user_id = 1 ORDER BY m.sort;查询为空就回数据库给管理员绑定角色并配菜单;查询有数据但前端仍空白,就对照前端路由目录修正 component 路径。另外注意菜单表里 sort 字段决定显示顺序,全为 0 时排序不稳定,不会导致空白但会让显示顺序很乱。
5.4 登录接口 500:Redis 连接失败导致系统异常
现象:后端正常启动,但一点登录就报系统异常,后台日志出现 RedisConnectionFailureException。
原因:验证码和 token 缓存都依赖 Redis,Redis 没启动或端口配错,登录链路直接断掉。常见于刚搭完环境,忘了把 redis-server 拉起来。
解决:本地执行 redis-server 启动,默认端口 6379,再用 redis-cli ping 确认返回 PONG。如果前期只想调接口不想依赖 Redis,可以在 config 里临时换成本地内存 Map 实现缓存,但这只能用于本地调试,部署环境必须 Redis,否则 token 拉黑和验证码校验都会失效。
5.5 时间差 8 小时:列表里的创建时间对不上
现象:接口返回的 createTime 比数据库里实际时间晚 8 小时,前端显示和日志对不上。
原因:MySQL 连接串没指定时区,驱动按 UTC 处理;JSON 序列化再按 GMT 时区输出,叠加起来就差 8 小时。
解决:数据源 url 加 serverTimezone=Asia/Shanghai,并在 application.yml 里补充配置:
spring: jackson: time-zone: GMT+8 date-format: yyyy-MM-dd HH:mm:ss改完重启,时间字段就正常了。这类问题没有明显报错,属于「数据看起来不对但没人报错」的隐性坑,我一般把时间字段检查固定加进验收清单,每次新环境部署都看一眼。
6. 二次开发实战:代码生成器接入业务表与三个改造方向
6.1 代码生成器:十分钟接入一张业务表
系统内置基于 MyBatis-Plus 的代码生成器,配好数据源和表名,自动生成实体、Mapper、Service、Controller 四层代码与前端页面。生成器配置的核心代码:
FastAutoGenerator.create("jdbc:mysql://localhost:3306/admin_db?serverTimezone=Asia/Shanghai", "root", "你的密码") .globalConfig(builder -> builder.author("dev").outputDir("src/main/java")) .packageConfig(builder -> builder.parent("com.example.admin").moduleName("biz")) .strategyConfig(builder -> builder.addInclude("biz_orders")) .execute();执行后把生成的 biz 包并入工程,重启,在菜单管理里给角色分配新菜单,新模块即可使用。生成后检查 entity 里日期字段是否带 @JsonFormat 注解,没有就自己补上,不然序列化格式跟全局配置对不上。
6.2 三个值得动手的改造方向
接入新表后先别碰前端,直接用接口文档调新增接口做一遍增删改查,确认权限注解生效后再挂菜单。改造优先级最高的三个方向:
一是 token 改造,把 Redis 里存储的单值字符串换成包含用户信息的对象,支持同账号多端登录互不挤掉线;二是多租户隔离,给业务表加 tenant_id 字段,接入 MyBatis-Plus 自带的租户拦截器,查询自动带租户条件;三是认证框架替换,把 Shiro 换成 Spring Security,对接公司统一的认证中心。这三个方向都只动 config 层和工具类,业务代码基本不动,正是这套系统分层清晰带来的好处。
当初我第一次拆这类项目的时候,急着改业务代码,结果权限接口全线 401,把过滤器链路从头捋了一遍才发现是 token 头名称前后端不一致。从那以后我每次拿到新脚手架,都先按第 4 章的验证清单走一遍登录、授权、登出,确认链路通了再动业务代码。希望帮到你。
本文还有配套的精品资源,点击获取