Spring Security+Redis后台管理系统:认证授权与会话管理实战 简介这是一套面向Java Web开发者与企业级应用学习者的完整后台管理系统源码基于Spring Boot、MyBatis、Spring MVC、Spring Security与Redis构建适合希望掌握现代Java技术栈协同开发的中级开发者参考实践。项目将持久层映射、MVC分层、认证授权与缓存加速串联为可运行的整体能帮助读者理解各组件在真实系统中的职责边界与集成方式。压缩包共326个文件约1.76MB以95个Java源码为核心辅以45个JavaScript、33个HTML与19个CSS构成前端交互与页面结构另有12个XML配置、1个SQL脚本及图片、字体等静态资源目录组织清晰便于按模块查阅。目前已有959人学习下载。通过研读源码读者可掌握Spring Security登录验证与权限控制流程、MyBatis定制化SQL映射写法以及Redis缓存高频数据的落地思路为构建复杂企业级应用积累可复用的工程经验。1. 后台管理系统为什么值得重做一遍接手一个后台管理系统最怕的不是功能多而是权限乱。我见过一个课程设计登录只判断了“是不是管理员”结果普通运营账号能直接调删除接口前端藏了按钮后端却毫无拦截——这就是典型的把权限当装饰。这套基于 Spring Boot MyBatis Spring MVC Spring Security Redis 的完整网站后台管理系统要解决的核心就是“认证、授权、会话、数据”四件事在一个工程里怎么摆正位置。它适合正在做毕设或课程设计、需要一套能跑通又能讲清原理的读者也适合刚进公司要独立交付一个后台骨架的开发者。下面按需求、架构、模块、避坑、验收的顺序把这份方案拆开讲透。2. 需求与选型为什么是这套技术组合2.1 后台系统的真实需求边界后台管理系统看起来千篇一律但真正决定工程结构的是几个容易被忽略的需求点。第一是角色与权限的粒度超级管理员、普通管理员、只读运营这三类账号能看到的菜单和能调的接口必须不同而且权限要能配置不能写死在代码里。第二是会话状态用户登录后菜单、按钮权限、登录态需要在多次请求间保持一致同时要支持强制下线。第三是数据操作的审计谁在什么时候删了哪条记录出问题要能查。第四是并发下的稳定性后台往往多人同时操作缓存和数据库的一致性不能靠运气。这些需求决定了技术选型不能只图“能跑”。如果只用 Session 存登录态多实例部署时就得做会话共享如果权限判断散落在每个 Controller 里后期加一个角色就要改几十处。所以这套方案把认证授权交给 Spring Security把会话和热点数据交给 Redis把持久层交给 MyBatisSpring MVC 负责请求编排Spring Boot 负责把这一切装配起来。选型不是堆技术而是每个组件对应一个明确的责任。2.2 各组件在方案里的职责划分把五个技术点摆在一起最容易犯的错是职责重叠。我一般的划分是这样的Spring Security 只做两件事——认证你是谁和授权你能干什么它的过滤器链是请求进入业务代码前的第一道闸门Redis 存登录令牌与用户权限快照让每次请求不必回查数据库MyBatis 负责 SQL 与对象映射复杂查询用 XML 写清楚Spring MVC 的 Controller 只做参数校验和结果封装不写业务判断Spring Boot 负责自动配置与依赖管理把数据源、Redis 连接、安全配置统一收口。这样划分的好处是任何一个环节出问题都能快速定位。比如登录后菜单不显示先看 Redis 里的权限快照有没有写进去再看 Security 的授权规则有没有放行而不是在 Controller 里到处打日志。下面这张表是各组件与需求的对应关系做方案文档时可以直接引用。组件承担职责对应需求不负责的事Spring Security认证、授权、密码加密角色权限隔离业务数据查询Redis令牌存储、权限快照、热点缓存会话一致性、强制下线持久化业务数据MyBatisSQL 映射、动态查询数据审计、分页查询事务边界控制Spring MVC请求路由、参数校验接口规范权限判断Spring Boot自动装配、配置管理工程可维护性业务逻辑2.3 版本与依赖的取舍原则版本选择上通常的做法是让 Spring Boot 的父 POM 统一管理 Spring Security、Spring MVC 的版本避免手动指定导致冲突。MyBatis 用 mybatis-spring-boot-starterRedis 用 spring-boot-starter-data-redis连接池选 Lettuce 即可。这里有个血泪经验不要同时引入两个 Redis 客户端依赖否则自动配置会打架启动时报连接工厂重复定义的错。依赖清单保持精简能少一个 starter 就少一个后台系统不需要为了“看起来技术栈丰富”而引入用不上的组件。3. 总体架构设计分层结构与请求链路3.1 四层结构与包组织方式这套后台的代码结构按职责分成四层接口层controller、业务层service、持久层mapper、实体与传输对象entity / dto / vo。包名按业务模块划分比如system、content、log每个模块内部再分 controller、service、mapper而不是全工程一个大 controller 包。这样做的原因是后台系统模块多按业务纵切比按技术横切更容易维护加一个模块就是加一个包不会牵一发动全身。安全相关的配置单独放config包包括 Security 配置、Redis 序列化配置、MyBatis 分页插件配置。实体类不直接返回给前端统一用 VO 转换避免把密码字段、内部状态字段暴露出去。这个习惯在答辩时很加分因为评委常问“你怎么防止敏感字段泄露”答案就在这层转换里。3.2 一次请求的完整链路理解请求链路是排查问题的前提。一个带登录态的请求进来顺序是这样的请求先经过 Spring Security 的过滤器链JwtAuthenticationFilter或令牌校验过滤器从请求头取出令牌去 Redis 查对应的用户信息与权限查到则构造认证对象放入 SecurityContext查不到直接返回 401。认证通过后进入 Spring MVC 的 DispatcherServlet路由到对应 ControllerController 调 ServiceService 调 Mapper 访问数据库结果逐层封装返回。这条链路里有两个关键点一是过滤器必须在业务代码之前执行所以权限拦截不依赖 Controller 里的 if 判断二是 Redis 查询失败要有降级策略不能因为缓存挂了整个后台都登不进去。我一般会在过滤器里对 Redis 异常做捕获记录日志后走数据库回查虽然慢一点但保证可用。下面这段是过滤器里校验令牌的核心逻辑注释写清了每一步的意图。Component public class TokenAuthFilter extends OncePerRequestFilter { Autowired private RedisTemplateString, Object redisTemplate; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 1. 从请求头取令牌约定 header 名为 Authorization String token request.getHeader(Authorization); if (token null || token.isEmpty()) { chain.doFilter(request, response); // 无令牌放行由后续授权规则决定是否拦截 return; } // 2. 以令牌为 key 查 Redis 中的登录用户快照 String key login:token: token; Object cached null; try { cached redisTemplate.opsForValue().get(key); } catch (Exception e) { // 缓存异常不阻断请求记录后走数据库回查降级 log.warn(redis query failed, fallback to db, e); } if (cached ! null) { LoginUser user (LoginUser) cached; // 3. 构造认证对象权限列表来自快照避免每次回查数据库 UsernamePasswordAuthenticationToken auth new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); } chain.doFilter(request, response); } }这段代码里令牌的 key 前缀统一为login:token:方便后续按前缀批量清理权限列表存在LoginUser里实现UserDetails接口getAuthorities()返回角色与权限集合。降级分支是很多人漏掉的缓存一抖整个后台登录态全丢用户会以为系统崩了。参数上令牌有效期建议在 Redis 里设置 TTL比如 30 分钟滑动过期每次请求命中后刷新 TTL这样既保证安全又不会让活跃用户频繁重登。3.3 权限模型的数据结构设计权限模型用经典的 RBAC用户表、角色表、权限表外加两张关联表。用户与角色多对多角色与权限多对多。权限的粒度落到“菜单 按钮 接口”三层菜单权限控制左侧导航显示按钮权限控制页面元素接口权限由 Spring Security 的PreAuthorize或配置类拦截。三层的权限编码统一格式比如system:user:delete前端和后端用同一套编码避免两边对不上。登录成功后把用户的角色和权限编码一次性查出来组装成LoginUser存入 Redis。这样每次请求的授权判断只读缓存不碰数据库。权限变更时比如管理员改了某个角色的权限主动删除相关用户的缓存 key下次请求重新加载。这个“写时失效”的策略比定时刷新更及时实现也简单。数据表设计上关联表加联合主键防止重复授权权限表加父级 ID 支持树形菜单这些细节在答辩时被问到能直接答上来。4. 核心模块实现认证、授权与数据层4.1 登录认证与令牌签发登录接口不经过 Security 的默认表单登录而是自定义一个/auth/login接口接收用户名密码用AuthenticationManager做认证成功后生成令牌写入 Redis。密码用 BCrypt 加密存储数据库里不存明文这是底线。认证失败要区分“用户不存在”和“密码错误”吗从安全角度不建议区分统一返回“用户名或密码错误”防止被枚举账号。令牌生成用 UUID 加时间戳即可不必上 JWT因为登录态已经存在 Redis 里令牌只是个索引。这样做的好处是服务端可以随时让令牌失效删 Redis key 就是强制下线而纯 JWT 做不到即时失效。下面这段是登录服务的核心实现注意认证成功后写缓存的顺序和 TTL 设置。Service public class AuthService { Autowired private AuthenticationManager authenticationManager; Autowired private RedisTemplateString, Object redisTemplate; Autowired private UserMapper userMapper; public String login(String username, String password) { // 1. 交给 Security 的认证管理器校验账号密码 Authentication authentication authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(username, password)); LoginUser loginUser (LoginUser) authentication.getPrincipal(); // 2. 查询该用户的角色与权限编码组装权限快照 loginUser.setPermissions(userMapper.selectPermissionsByUserId(loginUser.getUserId())); // 3. 生成令牌并写入 RedisTTL 30 分钟后续请求命中后刷新 String token UUID.randomUUID().toString().replace(-, ); String key login:token: token; redisTemplate.opsForValue().set(key, loginUser, 30, TimeUnit.MINUTES); return token; } }参数说明AuthenticationManager由 Security 配置类注入内部调用UserDetailsService加载用户selectPermissionsByUserId是一条多表关联查询返回权限编码列表TTL 设为 30 分钟是经验值太短用户频繁掉线太长强制下线不及时。刷新 TTL 的动作放在过滤器里命中缓存后调用expire重置形成滑动过期。这里有个容易翻车的点LoginUser存入 Redis 前必须可序列化否则取出来反序列化报错通常用 JDK 序列化或 JSON 序列化选一种并在配置里统一。4.2 接口级授权与菜单动态渲染接口授权用 Spring Security 的方法级注解在 Controller 方法上标PreAuthorize(hasAuthority(system:user:delete))权限编码与数据库里的一致。这样加一个接口权限只需加注解和配数据不用改安全配置类。菜单渲染则是登录后调/auth/menus接口根据当前用户的权限编码过滤菜单树只返回有权限的节点前端据此生成导航。按钮权限同理前端拿到权限编码列表后控制按钮显隐但后端接口必须独立校验不能只靠前端藏按钮。这里的设计要点是“前端控制体验后端控制安全”。很多人做课设时只做了前端隐藏后端接口裸奔一测就穿。正确的做法是两边用同一套权限编码前端从登录返回的权限列表里判断后端用注解拦截数据来源一致就不会出现“前端能点、后端拒绝”的尴尬。菜单树的构建在 Service 层完成先查全量菜单再按用户权限过滤最后递归组装成树递归深度一般不超过三层不用担心性能。4.3 MyBatis 持久层与分页查询持久层用 MyBatis 的 XML 映射复杂查询多表关联、动态条件写在 XML 里简单单表操作用注解或通用 Mapper。分页用 PageHelper 插件在 Service 层调用PageHelper.startPage(pageNum, pageSize)后紧跟查询返回的PageInfo直接封装给前端。这里有个坑startPage必须紧挨着查询语句中间插了别的查询会导致分页错乱这是 PageHelper 的线程变量机制决定的血泪经验。动态 SQL 用if和where标签拼条件比如用户列表按用户名、状态、角色筛选条件为空就不拼。要注意的是拼接条件时参数用#{}而不是${}后者有 SQL 注入风险。审计日志表用 AOP 切面在 Service 方法执行后异步写入记录操作人、接口、参数摘要、耗时参数里的密码字段要脱敏。下面是一个带条件的分页查询示例展示了 XML 里动态条件的写法。select idselectUserPage resultTypecom.example.system.vo.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 testusername ! null and username ! AND u.username LIKE CONCAT(%, #{username}, %) /if if teststatus ! null AND u.status #{status} /if if testroleId ! null AND r.id #{roleId} /if /where ORDER BY u.id DESC /select这段 SQL 的要点用 LEFT JOIN 保证没有角色的用户也能查出来where自动处理第一个 ANDLIKE 用 CONCAT 拼接兼容不同数据库排序字段固定避免分页结果不稳定。参数说明username模糊匹配status精确匹配roleId按角色过滤三个条件都可为空。Service 层调用时先startPage再执行返回PageInfoUserVO前端拿到total和list渲染表格和分页条。4.4 Redis 缓存策略与一致性处理Redis 在这套系统里承担三块登录令牌、权限快照、热点数据缓存比如字典表、系统配置。令牌和权限快照前面讲过热点缓存的策略是“读多写少的数据缓存写操作后删缓存”。比如字典表查询频繁但很少改查询时先读缓存未命中查库后写缓存并设 TTL字典更新时删除对应 key下次查询重新加载。不要用“更新缓存”代替“删除缓存”并发下容易写出旧值。缓存 key 的命名统一用模块:业务:标识的格式比如sys:dict:status方便排查和批量清理。序列化方式统一配置key 用 String 序列化value 用 JSON 序列化这样在 Redis 客户端里能直接看懂内容排查问题时不用反序列化。缓存穿透的防护对空结果也缓存一个短 TTL 的空值标记避免同一个不存在的 key 反复打数据库。这些细节写进方案文档能体现工程思维而不是“加了个 Redis 就完事”。5. 避坑与排查这套组合最容易翻车的地方5.1 权限注解不生效现象Controller 方法上加了PreAuthorize但无权限的账号照样能调通。原因通常是没开方法级安全Security 配置类上缺了EnableGlobalMethodSecurity(prePostEnabled true)或者注解加在了接口上而实现类没继承。解决在配置类开启注解支持注解统一加在 Controller 方法上测试时用低权限账号实际调一次接口验证别只看代码。5.2 登录态在重启后全部丢失现象服务重启后所有用户被踢下线需要重新登录。原因令牌只存在 Redis 里没问题但如果 Redis 没做持久化或者开发时用了内存版 Redis重启数据就没了。解决生产环境给 Redis 开 RDB 或 AOF 持久化开发环境接受这个行为但要在文档里写清楚避免答辩时被问“重启后登录态怎么办”答不上来。另一种情况是令牌存在本地 Map 里那重启必丢必须换成 Redis。5.3 分页查询总数不对现象列表数据正确但总条数显示为当前页条数分页条只显示一页。原因PageHelper 的startPage后面跟了多条查询或者查询返回后手动转换了集合导致PageInfo丢失分页信息。解决startPage紧跟目标查询返回结果直接交给PageInfo包装不要在中间做stream转换后再包装。排查时打印PageInfo的total和pageNum一眼能看出问题。5.4 缓存与数据库数据不一致现象后台改了配置前台读到的还是旧值。原因更新数据库后没删缓存或者删缓存的时机在事务提交之前事务回滚了缓存却已删这个方向反而安全更常见的是压根没删。解决写操作在事务提交后删缓存用TransactionSynchronization的afterCommit回调或者简单点Service 方法上加事务方法内先更新库再删缓存保证删缓存发生在事务内但库已更新。排查时对比 Redis 里的值和数据库的值定位是没删还是删错 key。5.5 跨域与令牌头丢失现象前端本地调试时接口 401但用 Postman 调同一个接口正常。原因浏览器跨域请求默认不带自定义头或者预检请求OPTIONS被 Security 拦截返回 401。解决Security 配置里放行 OPTIONS 请求CORS 配置允许前端来源和Authorization头。这个坑在前后端分离的后台里几乎必踩配置一次记一辈子。6. 验收与答辩怎么证明这套后台真的能用验收这套系统我一般不看页面多漂亮而是按“权限、会话、数据、异常”四条线走一遍。权限线准备三个不同角色的账号分别登录核对菜单显示、按钮显隐、接口调用三处是否一致重点用低权限账号直接调高权限接口必须返回 403。会话线登录后手动删掉 Redis 里的令牌 key再发请求应返回 401修改某角色权限后该角色用户下次请求应拿到新权限验证缓存失效逻辑。数据线新增、修改、删除各一条记录核对数据库和列表页一致分页总数正确审计日志有对应记录。异常线停掉 Redis 再登录看是否有友好提示而不是白屏堆栈。答辩时评委最爱问的三个问题提前准备好答案一是“你的权限是怎么拦截的”答过滤器链加方法注解前端只做体验二是“令牌为什么不用 JWT”答需要服务端可控的强制下线Redis 存快照更灵活三是“缓存和数据库怎么保证一致”答写后删缓存加事务提交后回调。下面这张验收清单可以直接放进文档逐项打勾。验收项操作步骤预期结果角色隔离低权限账号调删除接口返回 403数据未变强制下线删除 Redis 令牌后请求返回 401跳转登录权限变更改角色权限后重新请求菜单与接口权限同步更新分页正确翻到第二页并筛选总数与筛选结果一致缓存降级停 Redis 后登录有提示不抛未捕获异常有个习惯我从做第一个后台项目起就强制自己走一遍每次改完权限相关代码一定用最低权限的账号把所有菜单点一遍、把所有按钮对应的接口用工具调一遍确认该拦的都拦住了。这套动作花不了十分钟但能挡住答辩现场最尴尬的“演示时越权成功”。希望这套结构和清单能帮到你把方案落到能跑、能讲、能验收的程度。本文还有配套的精品资源点击获取