☰
Spring Boot + MyBatis + Spring Security + Redis 后台管理系统实战:权限闭环与集群会话
2026/10/30 5:09:39 网站建设 项目流程

简介:这是一套面向Java Web开发者与企业级应用学习者的完整后台管理系统源码,基于Spring Boot、MyBatis、Spring MVC、Spring Security与Redis构建,适合希望掌握现代Java技术栈协同开发的中高级学习者参考实践。项目将持久层映射、MVC分层、认证授权与缓存加速整合于一体,可帮助读者理解权限控制、会话管理与高频数据缓存等典型场景的实现思路。压缩包共326个文件,以95个Java源码为核心,辅以45个JavaScript、33个HTML与19个CSS构成前端页面与交互,另有12个XML配置、1个SQL脚本及图片字体等静态资源,整体约1.76MB,结构紧凑便于按模块研读。目前已有959人学习下载,可作为课程设计或企业级项目练手的参考素材。

1. 后台管理系统选型:为什么这套 Spring 全家桶组合至今仍是交付首选

接手一个网站后台管理系统的需求时,最先要定的不是页面长什么样,而是权限模型怎么落、会话状态放哪、数据访问层怎么组织。很多同学一上来就堆功能,结果做到一半发现菜单权限和接口权限两套逻辑打架,登录状态在集群下失效,最后返工重写。这套基于 Spring Boot + MyBatis + Spring MVC + Spring Security + Redis 的完整后台管理系统方案,解决的正是「一套能直接交付、权限闭环、可横向扩展」的问题。它适合正在做毕设或课程设计的学生,也适合需要快速出一版可运行后台的开发者。关键词里的 Springboot 整合方案,本质是把认证、授权、数据访问、缓存四件事拆清楚再拼起来,而不是简单堆依赖。

2. 需求与总体架构:认证、授权、数据、缓存四层怎么切

后台管理系统的需求看着杂,拆开其实就四类:谁能进系统(认证)、进来能看什么能点什么(授权)、业务数据怎么增删改查(数据访问)、高频数据怎么不反复打库(缓存)。这四类需求对应四个技术层,Spring Security 管前两层,MyBatis 管第三层,Redis 管第四层,Spring MVC 负责把 HTTP 请求翻译成这四层的调用。先把这条主线立住,后面每个模块才有地方挂。

2.1 分层结构与模块职责划分

我一般把整个系统切成五块:入口层、安全层、业务层、数据层、缓存层。入口层是 Controller,只做参数校验和响应封装,不写业务判断;安全层是 Spring Security 的过滤链和自定义的 UserDetailsService、AccessDecisionManager;业务层是 Service,事务边界放在这里;数据层是 MyBatis 的 Mapper 接口和 XML;缓存层是 Redis 的封装工具类,统一 key 前缀和过期策略。

这样切的好处是权限判断不会散落在各个 Controller 里。血泪经验是,一旦在 Controller 里手写if (user.getRole().equals("admin")),后面加角色就得全局搜一遍改一遍,迟早翻车。把判断收进安全层,Controller 只关心「这个请求该返回什么数据」。

模块之间的依赖方向是单向的:入口层依赖安全层和业务层,业务层依赖数据层和缓存层,数据层和缓存层互不依赖。这条约束写进项目规范,新人接手时不容易把缓存调用塞进 Mapper 里。

2.2 技术选型理由与版本组合

选 Spring Boot 而不是裸 Spring,核心原因是自动装配省掉了大量 XML 配置,后台系统这种「配置多、业务相对标准」的场景收益最大。MyBatis 而不是 JPA,是因为后台系统大量列表查询带动态条件,XML 里写动态 SQL 比拼 Criteria 直观得多,分页插件也成熟。Spring Security 而不是自己写拦截器,是因为它把认证、授权、CSRF、会话管理做成了可组合的过滤器链,自己写容易漏。

Redis 在这里承担两个职责:一是存登录会话和 token,让系统在多加一个节点时不用改代码;二是缓存菜单树、字典数据这类读多写少的数据。版本组合上,Spring Boot 2.x 搭配 Spring Security 5.x、MyBatis 3.x、Redis 6.x 是经过大量项目验证的稳定组合,具体小版本按依赖管理走,不要手动锁死某个补丁号,否则升级时容易冲突。

提示:安全层和缓存层的初始化顺序有讲究,Security 的 UserDetailsService 里如果要查 Redis 里的会话,必须保证 Redis 连接池先就绪,否则启动时偶发空指针。

2.3 数据库表与权限模型设计

权限模型用经典的 RBAC:用户表、角色表、权限表,外加两张关联表。用户和角色多对多,角色和权限多对多,权限粒度落到「菜单 + 按钮 + 接口」三级。菜单权限控制左侧导航显示什么,按钮权限控制页面上哪个操作按钮可见,接口权限在服务端兜底,三者用同一套权限编码串起来。

核心表结构大致如下:

表名关键字段说明
sys_userid, username, password, status用户主表,密码存 BCrypt 哈希
sys_roleid, role_code, role_name角色表,role_code 用于程序判断
sys_permissionid, perm_code, perm_type, parent_id权限表,perm_type 区分菜单/按钮/接口
sys_user_roleuser_id, role_id用户角色关联
sys_role_permissionrole_id, perm_id角色权限关联

perm_code 用「模块:资源:动作」的格式,比如system:user:add,这样接口上的注解和前端按钮的标识能对上,排查权限问题时一眼能看出缺哪一段。parent_id 支撑菜单树的层级,菜单和按钮共用一张表,靠 perm_type 区分,减少表数量。

3. 安全模块落地:Spring Security 过滤链与 Redis 会话的配合

安全模块是整个后台系统最容易出问题的地方,也是答辩时最容易被追问的地方。这一章把认证流程、授权判断、会话存储三件事拆开讲,每块给出可抄的配置和代码,并说明参数怎么改、失败时看什么。

3.1 认证流程与过滤链配置

认证流程是:用户提交用户名密码 → 过滤器拦截 → UserDetailsService 查库 → 密码比对 → 生成认证对象存入 SecurityContext → 会话写入 Redis。这条链上任何一环配错,表现都是「登录一直跳回登录页」,所以配置要写得清楚。

@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsServiceImpl userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http // 关闭 CSRF,前后端分离场景下改用 token 校验 .csrf().disable() .authorizeRequests() // 登录、验证码等接口放行 .antMatchers("/login", "/captcha/**").permitAll() // 静态资源放行 .antMatchers("/static/**", "/favicon.ico").permitAll() // 其余接口要求已认证 .anyRequest().authenticated() .and() .formLogin() .loginProcessingUrl("/login") .successHandler(new JsonAuthSuccessHandler()) .failureHandler(new JsonAuthFailureHandler()) .and() .logout() .logoutUrl("/logout") .logoutSuccessHandler(new JsonLogoutHandler()) .and() // 会话交给 Spring Session + Redis 管理 .sessionManagement() .maximumSessions(1) .maxSessionsPreventsLogin(false); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 指定自定义 UserDetailsService 和密码编码器 auth.userDetailsService(userDetailsService) .passwordEncoder(new BCryptPasswordEncoder()); } }

这段配置的关键点:loginProcessingUrl要和前端表单提交地址一致,不一致就是 404 或一直跳登录;successHandler和failureHandler换成返回 JSON 的实现,否则前后端分离下前端拿到的是重定向页面而不是可解析的结果;maximumSessions(1)限制单账号同时在线数,maxSessionsPreventsLogin(false)表示新登录踢掉旧会话,后台系统一般用这个策略。

密码编码器必须和存库时的编码方式一致,存的是 BCrypt 哈希,比对时也用 BCrypt,混用 MD5 会导致「密码明明对却登录失败」这种玄学问题。

3.2 接口级权限控制与注解使用

认证解决「你是谁」,授权解决「你能干什么」。接口级权限用@PreAuthorize注解,表达式里写权限编码,和数据库里的 perm_code 对应。

@RestController @RequestMapping("/system/user") public class UserController { @Autowired private UserService userService; // 需要 system:user:list 权限才能查询 @PreAuthorize("hasAuthority('system:user:list')") @GetMapping("/list") public Result list(UserQuery query) { return Result.ok(userService.page(query)); } // 需要 system:user:add 权限才能新增 @PreAuthorize("hasAuthority('system:user:add')") @PostMapping("/add") public Result add(@RequestBody @Valid UserForm form) { userService.add(form); return Result.ok(); } }

要让hasAuthority生效,UserDetailsService 返回的 UserDetails 里必须把权限编码塞进 authorities 集合,且前缀不要多加ROLE_,否则表达式要改成hasRole,两者混用是常见翻车点。权限编码从数据库查出来后,建议在登录时一次性加载进会话,避免每次请求都查库;但角色权限变更后要主动刷新会话,否则改了权限不生效,用户会以为系统坏了。

3.3 Redis 会话存储与集群下的登录态

单机部署时用 HttpSession 没问题,一旦加节点,会话不共享就会「登录后刷新又掉线」。用 Spring Session 把会话存进 Redis,配置很少:

spring: redis: host: 127.0.0.1 port: 6379 database: 0 timeout: 3000ms session: store-type: redis timeout: 1800s redis: namespace: admin:session

store-type: redis开启 Redis 会话存储,timeout是会话过期时间,后台系统一般设 30 分钟,太短用户频繁掉线,太长安全性下降。namespace给 key 加前缀,避免和业务缓存混在一起,排查时用keys admin:session*能快速定位。

会话里存的对象必须可序列化,自定义的 UserDetails 实现类要实现 Serializable,否则写入 Redis 时报序列化异常,日志里是一长串看不懂的堆栈,这就是典型的黑匣子问题。我一般会在 UserDetails 实现类上显式写serialVersionUID,改字段时手动维护,避免反序列化失败。

4. 业务与数据层实现:MyBatis 动态查询与缓存策略

安全层通了之后,业务层和数据层是工作量最大的部分。后台系统的业务特点很集中:列表分页带多条件筛选、详情查询、增删改、字典和菜单这类基础数据。这一章讲 MyBatis 怎么组织、分页怎么做、缓存怎么加才不出脏数据。

4.1 MyBatis Mapper 组织与动态 SQL

Mapper 按业务模块划分,一个模块一个接口一个 XML,接口方法名和 XML 的 id 一一对应。动态查询用<where>和<if>组合,避免手拼1=1。

<select id="selectUserPage" resultType="UserVO"> SELECT u.id, u.username, u.status, r.role_name FROM sys_user u LEFT JOIN sys_user_role ur ON u.id = ur.user_id LEFT JOIN sys_role r ON ur.role_id = r.id <where> <if test="username != null and username != ''"> AND u.username LIKE CONCAT('%', #{username}, '%') </if> <if test="status != null"> AND u.status = #{status} </if> <if test="roleId != null"> AND ur.role_id = #{roleId} </if> </where> ORDER BY u.id DESC </select>

<where>会自动处理第一个AND,省掉手写1=1的丑写法。LIKE用CONCAT拼接而不是${},防止 SQL 注入,这是必须守住的底线。多表关联查询要注意结果去重,一个用户多个角色时会出现重复行,要么在 SQL 里GROUP BY,要么在 Service 层合并,我一般选前者,逻辑更集中。

分页用 PageHelper 插件,在查询前一行调用PageHelper.startPage(pageNum, pageSize),插件会自动改写 SQL 加LIMIT。参数校验要在 Controller 层做,pageNum 小于 1 时归一到 1,pageSize 设上限比如 100,防止有人传个超大值把库拖垮。

4.2 缓存设计与失效策略

缓存加在 Service 层,不加在 Mapper 层,因为 Mapper 是数据访问的最小单元,缓存应该跟着业务语义走。菜单树、字典数据、角色权限这三类适合缓存,用户列表这种带动态条件的查询不适合缓存,命中率低还容易脏。

@Service public class MenuService { @Autowired private MenuMapper menuMapper; @Autowired private RedisTemplate<String, Object> redisTemplate; private static final String MENU_TREE_KEY = "admin:cache:menu:tree"; public List<MenuNode> getMenuTree() { // 先查缓存 Object cached = redisTemplate.opsForValue().get(MENU_TREE_KEY); if (cached != null) { return (List<MenuNode>) cached; } // 缓存未命中查库 List<MenuNode> tree = buildTree(menuMapper.selectAll()); // 写入缓存,设置 1 小时过期 redisTemplate.opsForValue().set(MENU_TREE_KEY, tree, 1, TimeUnit.HOURS); return tree; } // 菜单变更时主动删缓存,下次查询重建 public void refreshMenuCache() { redisTemplate.delete(MENU_TREE_KEY); } }

缓存策略是「读时重建 + 写时删除」,不做写时更新,因为更新缓存容易在并发下写出旧值。菜单增删改后调用refreshMenuCache删 key,下次读自然重建。过期时间设 1 小时是兜底,防止某次删除失败导致缓存永久脏下去。

注意:RedisTemplate 的序列化器要统一配置,key 用 String 序列化,value 用 JSON 序列化,否则存进去的对象在 redis-cli 里是一堆乱码,排查时根本看不懂。

4.3 事务边界与异常处理

事务加在 Service 的写方法上,用@Transactional,传播行为默认 REQUIRED。要注意的是,同一个类内部方法互相调用时事务注解不生效,这是 Spring AOP 代理机制决定的,踩过这个坑的人不少。解决办法是把需要事务的方法拆到另一个 Service,或者注入自身代理。

异常处理用全局异常处理器@RestControllerAdvice,把业务异常、参数校验异常、权限异常分别映射成统一的响应结构。权限不足抛AccessDeniedException,全局处理器里捕获后返回 403 和提示信息,而不是让 Spring Security 默认跳到一个错误页,前后端分离下前端拿不到可解析的内容。

5. 避坑与排查:这套组合最容易翻车的五个点

这套技术组合单独用都不难,拼在一起时问题往往出在交界处。下面五个点是我在项目里反复见到的,按「现象 → 原因 → 解决」写清楚,照着排查能省不少时间。

5.1 登录成功却一直跳回登录页

现象是输入正确账号密码,接口返回成功,但下一个请求又被拦截。原因通常是会话没存住,或者前端没带会话标识。先看 Redis 里有没有admin:session开头的 key,没有就是 Spring Session 没生效,检查store-type配置和 Redis 连接;有 key 但请求还是被拦,检查前端请求是否带了 Cookie,跨域场景下要配置withCredentials和服务端的allowCredentials。还有一种情况是 Security 配置里anyRequest().authenticated()把登录接口自己也拦了,确认permitAll的路径写对。

5.2 权限注解不生效

现象是加了@PreAuthorize但没权限的请求照样能进。原因一是没开方法级安全,启动类或配置类上缺@EnableGlobalMethodSecurity(prePostEnabled = true);原因二是 UserDetails 里的 authorities 为空,登录时没把权限查出来塞进去。排查时在@PreAuthorize方法里打个断点看SecurityContextHolder.getContext().getAuthentication().getAuthorities(),空的就是加载环节的问题。

5.3 缓存和数据库数据不一致

现象是改了菜单,页面上还是旧的。原因是只更新了库没删缓存,或者删缓存的操作在事务提交前执行,事务回滚后缓存已经删了但库没变。解决是把删缓存放在事务提交之后,用TransactionSynchronizationManager.registerSynchronization注册回调,或者干脆接受短暂不一致,靠过期时间兜底。后台系统对一致性要求没那么极端,我一般选后者,简单可靠。

5.4 分页查询总数不对

现象是列表数据对但总条数偏大。原因是关联查询产生了重复行,COUNT把重复的也算进去了。PageHelper 对复杂关联查询的 count 语句有时会简化出错,解决办法是手写 count 查询,在 Mapper 里单独提供一个countUserPage方法,SQL 里用COUNT(DISTINCT u.id),然后关掉 PageHelper 的自动 count。

5.5 启动时报循环依赖

现象是应用起不来,日志提示 bean 循环依赖。这套组合里常见于 SecurityConfig 依赖 UserDetailsService,UserDetailsService 又依赖了某个注入了 SecurityConfig 的组件。解决是理清依赖方向,把公共的查询逻辑抽到独立的 Service,SecurityConfig 只依赖这个 Service,不要让配置类反向被业务依赖。Spring Boot 2.6 以后默认禁止循环依赖,这个报错会更早暴露,反而是好事。

6. 验收与答辩:怎么证明这套后台系统真的能交付

验收这套系统,我一般不看页面多花哨,看四件事能不能当场演示通过:登录认证闭环、权限控制生效、数据增删改查正确、缓存命中可观测。答辩时老师或评审最常问的是「你怎么保证接口安全」和「多节点下登录态怎么办」,这两个问题答清楚,基本就稳了。

先准备一份验收清单,逐项演示:

验收项操作预期结果
认证错误密码登录返回失败提示,不生成会话
认证正确密码登录返回成功,Redis 出现会话 key
授权用低权限账号访问高权限接口返回 403,不返回数据
授权修改角色权限后重新登录新权限立即生效
数据新增用户后列表查询新用户出现在列表,分页总数加一
缓存连续两次查菜单树第二次不查库,响应明显更快
会话同一账号二次登录旧会话失效,符合单点策略

演示时有个技巧:把 Redis 的监控开在一个窗口,操作时实时看 key 的变化,比口头说「我用了 Redis」有说服力得多。权限演示一定要用两个不同角色的账号对比,只用一个管理员账号演示等于没演示授权。

答辩被问到技术选型时,不要背概念,讲一个具体决策。比如「为什么用 Spring Security 不用拦截器」,就说拦截器只能拦 URL,做不到方法级权限,也做不到和会话管理、CSRF 防护的组合,后期加 OAuth2 还得重写。这种答法显得你真的做过取舍,而不是照抄教程。

还有一个容易被忽略的点:准备一份「已知问题与改进方向」的说明。比如当前会话存在单 Redis 节点,生产环境要考虑主从;权限变更后需要用户重新登录才完全生效,可以改成主动推送刷新。主动说出局限,比被问出来强。

从那以后我每次交付后台系统,都会在验收前自己按清单走一遍,尤其是权限和会话这两块,宁可多花半小时演示,也不要在答辩现场被一个 403 问住。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询