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,把持久层交给 MyBatis,Spring 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-starter,Redis 用 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,路由到对应 Controller,Controller 调 Service,Service 调 Mapper 访问数据库,结果逐层封装返回。

这条链路里有两个关键点:一是过滤器必须在业务代码之前执行,所以权限拦截不依赖 Controller 里的 if 判断;二是 Redis 查询失败要有降级策略,不能因为缓存挂了整个后台都登不进去。我一般会在过滤器里对 Redis 异常做捕获,记录日志后走数据库回查,虽然慢一点但保证可用。下面这段是过滤器里校验令牌的核心逻辑,注释写清了每一步的意图。

@Component public class TokenAuthFilter extends OncePerRequestFilter { @Autowired private RedisTemplate<String, 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 RedisTemplate<String, 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. 生成令牌并写入 Redis,TTL 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 id="selectUserPage" resultType="com.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 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 r.id = #{roleId} </if> </where> ORDER BY u.id DESC </select>

这段 SQL 的要点:用 LEFT JOIN 保证没有角色的用户也能查出来;<where>自动处理第一个 AND;LIKE 用 CONCAT 拼接,兼容不同数据库;排序字段固定,避免分页结果不稳定。参数说明:username模糊匹配,status精确匹配,roleId按角色过滤,三个条件都可为空。Service 层调用时先startPage再执行,返回PageInfo<UserVO>,前端拿到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 后登录有提示,不抛未捕获异常

有个习惯我从做第一个后台项目起就强制自己走一遍:每次改完权限相关代码,一定用最低权限的账号把所有菜单点一遍、把所有按钮对应的接口用工具调一遍,确认该拦的都拦住了。这套动作花不了十分钟,但能挡住答辩现场最尴尬的“演示时越权成功”。希望这套结构和清单能帮到你,把方案落到能跑、能讲、能验收的程度。

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